ghost 4.22.4 → 4.25.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.
Files changed (170) hide show
  1. package/.eslintrc.js +39 -0
  2. package/content/themes/casper/assets/built/casper.js +1 -1
  3. package/content/themes/casper/assets/built/casper.js.map +1 -1
  4. package/content/themes/casper/assets/built/global.css +1 -1
  5. package/content/themes/casper/assets/built/global.css.map +1 -1
  6. package/content/themes/casper/assets/built/screen.css +1 -1
  7. package/content/themes/casper/assets/built/screen.css.map +1 -1
  8. package/content/themes/casper/assets/css/global.css +6 -1
  9. package/content/themes/casper/assets/css/screen.css +32 -216
  10. package/content/themes/casper/default.hbs +2 -2
  11. package/content/themes/casper/package.json +3 -2
  12. package/content/themes/casper/post.hbs +1 -1
  13. package/content/themes/casper/yarn.lock +173 -123
  14. package/core/app.js +12 -1
  15. package/core/boot.js +33 -19
  16. package/core/bridge.js +10 -10
  17. package/core/built/assets/ghost-dark-f67240a9636407594be38571c615629c.css +1 -0
  18. package/core/built/assets/{ghost.min-2e3e64eb258cf424c59c3e308b4bc6e6.js → ghost.min-3441c3282e390002626a2dc1d7586185.js} +544 -619
  19. package/core/built/assets/ghost.min-ee5bd95a831378b4c8ccefb37d26eac0.css +1 -0
  20. package/core/built/assets/icons/audio-upload.svg +8 -0
  21. package/core/built/assets/{vendor.min-c9002845b6c30ac978abdadde9f33d7c.js → vendor.min-6fc912d1248c906f95efad2cb3eebb7d.js} +2656 -2118
  22. package/core/frontend/apps/amp/lib/helpers/amp_content.js +2 -2
  23. package/core/frontend/apps/amp/lib/views/amp.hbs +70 -0
  24. package/core/frontend/apps/private-blogging/index.js +1 -1
  25. package/core/frontend/helpers/url.js +18 -1
  26. package/core/frontend/services/apps/index.js +1 -1
  27. package/core/frontend/services/apps/loader.js +3 -3
  28. package/core/frontend/services/card-assets/index.js +0 -12
  29. package/core/frontend/services/card-assets/service.js +22 -21
  30. package/core/frontend/services/helpers/handlebars.js +1 -1
  31. package/core/frontend/services/theme-engine/middleware/ensure-active-theme.js +34 -0
  32. package/core/frontend/services/theme-engine/middleware/index.js +6 -0
  33. package/core/frontend/services/theme-engine/middleware/update-global-template-options.js +116 -0
  34. package/core/frontend/services/theme-engine/middleware/update-local-template-data.js +9 -0
  35. package/core/frontend/services/theme-engine/middleware/update-local-template-options.js +57 -0
  36. package/core/frontend/src/cards/css/blockquote.css +29 -0
  37. package/core/frontend/src/cards/css/bookmark.css +7 -0
  38. package/core/frontend/src/cards/css/button.css +4 -0
  39. package/core/frontend/src/cards/css/callout.css +23 -15
  40. package/core/frontend/src/cards/css/gallery.css +13 -3
  41. package/core/frontend/src/cards/css/toggle.css +36 -16
  42. package/core/frontend/web/middleware/error-handler.js +93 -0
  43. package/core/frontend/web/middleware/handle-image-sizes.js +3 -6
  44. package/core/frontend/web/middleware/index.js +1 -0
  45. package/core/frontend/web/middleware/serve-public-file.js +25 -8
  46. package/core/frontend/web/site.js +2 -5
  47. package/core/server/adapters/scheduling/SchedulingDefault.js +2 -2
  48. package/core/server/adapters/storage/LocalStorageBase.js +2 -2
  49. package/core/server/api/canary/db.js +2 -2
  50. package/core/server/api/canary/media.js +3 -2
  51. package/core/server/api/canary/oembed.js +16 -1
  52. package/core/server/api/canary/session.js +1 -1
  53. package/core/server/api/canary/slugs.js +1 -1
  54. package/core/server/api/canary/utils/permissions.js +2 -2
  55. package/core/server/api/canary/utils/serializers/output/config.js +2 -6
  56. package/core/server/api/v2/db.js +2 -2
  57. package/core/server/api/v2/session.js +1 -1
  58. package/core/server/api/v2/slugs.js +1 -1
  59. package/core/server/api/v2/utils/permissions.js +2 -2
  60. package/core/server/api/v3/db.js +2 -2
  61. package/core/server/api/v3/session.js +1 -1
  62. package/core/server/api/v3/slugs.js +1 -1
  63. package/core/server/api/v3/utils/permissions.js +2 -2
  64. package/core/server/data/db/state-manager.js +4 -4
  65. package/core/server/data/exporter/export-filename.js +1 -1
  66. package/core/server/data/importer/handlers/json.js +1 -1
  67. package/core/server/data/importer/import-manager.js +1 -1
  68. package/core/server/data/importer/importers/data/base.js +1 -1
  69. package/core/server/data/migrations/utils.js +2 -2
  70. package/core/server/data/migrations/versions/1.25/1-update-koenig-beta-html.js +1 -0
  71. package/core/server/data/migrations/versions/3.1/08-add-uuid-values-to-members.js +1 -0
  72. package/core/server/data/migrations/versions/3.22/02-settings-key-renames.js +2 -0
  73. package/core/server/data/migrations/versions/3.22/05-migrate-members-subscription-settings.js +3 -0
  74. package/core/server/data/migrations/versions/3.22/06-migrate-stripe-connect-settings.js +2 -0
  75. package/core/server/data/migrations/versions/3.23/01-migrate-bulk-email-settings.js +1 -0
  76. package/core/server/data/migrations/versions/3.29/01-remove-duplicate-subscriptions.js +2 -0
  77. package/core/server/data/migrations/versions/3.29/02-remove-duplicate-customers.js +2 -0
  78. package/core/server/data/migrations/versions/3.38/04-populate-recipient-filter-column.js +2 -0
  79. package/core/server/data/migrations/versions/4.0/01-update-mobiledoc.js +2 -0
  80. package/core/server/data/migrations/versions/4.0/03-populate-status-column-for-members.js +4 -0
  81. package/core/server/data/migrations/versions/4.0/06-populate-members-subscribe-events-table.js +1 -0
  82. package/core/server/data/migrations/versions/4.0/17-populate-members-status-events-table.js +1 -0
  83. package/core/server/data/migrations/versions/4.0/18-transform-urls-absolute-to-transform-ready.js +5 -0
  84. package/core/server/data/migrations/versions/4.0/22-solve-orphaned-webhooks.js +1 -0
  85. package/core/server/data/migrations/versions/4.0/23-regenerate-posts-html.js +1 -0
  86. package/core/server/data/migrations/versions/4.0/25-populate-members-paid-subscription-events-table.js +2 -1
  87. package/core/server/data/migrations/versions/4.12/02-fix-member-statuses.js +1 -0
  88. package/core/server/data/migrations/versions/4.14/01-fix-comped-member-statuses.js +3 -0
  89. package/core/server/data/migrations/versions/4.14/02-fix-free-members-status-events.js +1 -0
  90. package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +2 -0
  91. package/core/server/data/migrations/versions/4.23/01-truncate-offer-names.js +59 -0
  92. package/core/server/data/migrations/versions/4.3/04-attach-members-to-product.js +1 -0
  93. package/core/server/data/migrations/versions/4.4/01-restore-free-members-signup-setting-from-backup.js +1 -0
  94. package/core/server/data/migrations/versions/4.6/01-remove-comped-status.js +1 -0
  95. package/core/server/data/migrations/versions/4.8/04-migrate-show-newsletter-header-setting.js +1 -0
  96. package/core/server/data/migrations/versions/4.9/05-fix-missed-mobiledoc-url-transforms.js +1 -0
  97. package/core/server/data/migrations/versions/4.9/06-add-comped-status.js +1 -0
  98. package/core/server/data/migrations/versions/4.9/07-update-comped-members-status-events.js +1 -0
  99. package/core/server/data/schema/commands.js +2 -2
  100. package/core/server/ghost-server.js +2 -2
  101. package/core/server/lib/image/image-size.js +2 -2
  102. package/core/server/models/base/listeners.js +2 -2
  103. package/core/server/models/member-email-change-event.js +2 -2
  104. package/core/server/models/member-login-event.js +2 -2
  105. package/core/server/models/member-paid-subscription-event.js +3 -3
  106. package/core/server/models/member-payment-event.js +3 -3
  107. package/core/server/models/member-product-event.js +6 -6
  108. package/core/server/models/member-status-event.js +5 -3
  109. package/core/server/models/member-subscribe-event.js +9 -3
  110. package/core/server/models/relations/authors.js +1 -1
  111. package/core/server/models/settings.js +1 -1
  112. package/core/server/services/auth/passwordreset.js +1 -1
  113. package/core/server/services/auth/setup.js +1 -1
  114. package/core/server/services/email-analytics/jobs/index.js +1 -1
  115. package/core/server/services/mega/mega.js +6 -4
  116. package/core/server/services/mega/template.js +31 -12
  117. package/core/server/services/members/api.js +22 -0
  118. package/core/server/services/members/config.js +1 -1
  119. package/core/server/services/members/emails/signup-paid.js +168 -0
  120. package/core/server/services/members/service.js +6 -2
  121. package/core/server/services/members/stripe-connect.js +4 -2
  122. package/core/server/services/nft-oembed.js +6 -1
  123. package/core/server/services/oembed.js +36 -28
  124. package/core/server/services/permissions/can-this.js +1 -1
  125. package/core/server/services/redirects/api.js +20 -25
  126. package/core/server/services/redirects/index.js +18 -10
  127. package/core/server/services/redirects/utils.js +14 -0
  128. package/core/server/services/redirects/validation.js +10 -0
  129. package/core/server/services/route-settings/default-settings-manager.js +1 -1
  130. package/core/server/services/route-settings/index.js +40 -17
  131. package/core/server/services/route-settings/route-settings.js +120 -115
  132. package/core/server/services/route-settings/settings-loader.js +18 -36
  133. package/core/server/services/route-settings/yaml-parser.js +1 -1
  134. package/core/server/services/slack.js +1 -1
  135. package/core/server/services/themes/activation-bridge.js +3 -3
  136. package/core/server/services/themes/storage.js +2 -2
  137. package/core/server/services/twitter-embed.js +80 -0
  138. package/core/server/services/url/LocalFileCache.js +75 -0
  139. package/core/server/services/url/UrlService.js +15 -47
  140. package/core/server/services/url/index.js +17 -4
  141. package/core/server/services/xmlrpc.js +2 -2
  142. package/core/server/web/admin/app.js +2 -5
  143. package/core/server/web/admin/controller.js +35 -12
  144. package/core/server/web/admin/middleware/redirect-admin-urls.js +15 -0
  145. package/core/server/web/admin/views/default-prod.html +4 -4
  146. package/core/server/web/admin/views/default.html +4 -4
  147. package/core/server/web/api/canary/admin/app.js +0 -3
  148. package/core/server/web/api/canary/admin/middleware.js +1 -1
  149. package/core/server/web/api/canary/content/app.js +0 -3
  150. package/core/server/web/api/v2/admin/app.js +0 -3
  151. package/core/server/web/api/v2/admin/middleware.js +1 -1
  152. package/core/server/web/api/v2/content/app.js +0 -3
  153. package/core/server/web/api/v3/admin/app.js +0 -3
  154. package/core/server/web/api/v3/admin/middleware.js +1 -1
  155. package/core/server/web/api/v3/content/app.js +0 -3
  156. package/core/server/web/members/app.js +0 -3
  157. package/core/server/web/oauth/app.js +0 -4
  158. package/core/server/web/parent/app.js +17 -8
  159. package/core/server/web/shared/middleware/error-handler.js +57 -162
  160. package/core/server/web/shared/middleware/index.js +0 -4
  161. package/core/shared/config/defaults.json +7 -1
  162. package/core/shared/labs.js +10 -5
  163. package/core/shared/sentry.js +1 -1
  164. package/package.json +43 -42
  165. package/yarn.lock +802 -923
  166. package/content/themes/casper/assets/js/gallery-card.js +0 -24
  167. package/core/built/assets/ghost-dark-42cf6e0c730578940ec069bda45aea41.css +0 -1
  168. package/core/built/assets/ghost.min-fcf6a0738421f86c47c55f20d00c5ba9.css +0 -1
  169. package/core/frontend/services/theme-engine/middleware.js +0 -209
  170. package/core/server/web/shared/middleware/maintenance.js +0 -25
@@ -11,7 +11,9 @@ const MemberStatusEvent = ghostBookshelf.Model.extend({
11
11
  customQuery(qb, options) {
12
12
  if (options.aggregateStatusCounts) {
13
13
  if (options.limit || options.filter) {
14
- throw new errors.IncorrectUsageError('aggregateStatusCounts does not work when passed a filter or limit');
14
+ throw new errors.IncorrectUsageError({
15
+ message: 'aggregateStatusCounts does not work when passed a filter or limit'
16
+ });
15
17
  }
16
18
  const knex = ghostBookshelf.knex;
17
19
  return qb.clear('select')
@@ -46,11 +48,11 @@ const MemberStatusEvent = ghostBookshelf.Model.extend({
46
48
  return options;
47
49
  },
48
50
  async edit() {
49
- throw new errors.IncorrectUsageError('Cannot edit MemberStatusEvent');
51
+ throw new errors.IncorrectUsageError({message: 'Cannot edit MemberStatusEvent'});
50
52
  },
51
53
 
52
54
  async destroy() {
53
- throw new errors.IncorrectUsageError('Cannot destroy MemberStatusEvent');
55
+ throw new errors.IncorrectUsageError({message: 'Cannot destroy MemberStatusEvent'});
54
56
  }
55
57
  });
56
58
 
@@ -11,7 +11,9 @@ const MemberSubscribeEvent = ghostBookshelf.Model.extend({
11
11
  customQuery(qb, options) {
12
12
  if (options.aggregateSubscriptionDeltas) {
13
13
  if (options.limit || options.filter) {
14
- throw new errors.IncorrectUsageError('aggregateSubscriptionDeltas does not work when passed a filter or limit');
14
+ throw new errors.IncorrectUsageError({
15
+ message: 'aggregateSubscriptionDeltas does not work when passed a filter or limit'
16
+ });
15
17
  }
16
18
  const knex = ghostBookshelf.knex;
17
19
  return qb.clear('select')
@@ -32,11 +34,15 @@ const MemberSubscribeEvent = ghostBookshelf.Model.extend({
32
34
  return options;
33
35
  },
34
36
  async edit() {
35
- throw new errors.IncorrectUsageError('Cannot edit MemberSubscribeEvent');
37
+ throw new errors.IncorrectUsageError({
38
+ message: 'Cannot edit MemberSubscribeEvent'
39
+ });
36
40
  },
37
41
 
38
42
  async destroy() {
39
- throw new errors.IncorrectUsageError('Cannot destroy MemberSubscribeEvent');
43
+ throw new errors.IncorrectUsageError({
44
+ message: 'Cannot destroy MemberSubscribeEvent'
45
+ });
40
46
  }
41
47
  });
42
48
 
@@ -324,7 +324,7 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
324
324
  .then(() => response);
325
325
  })
326
326
  .catch((err) => {
327
- throw new errors.GhostError({err: err});
327
+ throw new errors.InternalServerError({err: err});
328
328
  });
329
329
  });
330
330
 
@@ -346,7 +346,7 @@ Settings = ghostBookshelf.Model.extend({
346
346
  );
347
347
 
348
348
  if (validationErrors.length) {
349
- throw new errors.ValidationError(validationErrors.join('\n'));
349
+ throw new errors.ValidationError({message: validationErrors.join('\n')});
350
350
  }
351
351
  },
352
352
  async labs(model) {
@@ -148,7 +148,7 @@ function doReset(options, tokenParts, settingsAPI) {
148
148
  return Promise.reject(err);
149
149
  })
150
150
  .catch((err) => {
151
- if (errors.utils.isIgnitionError(err)) {
151
+ if (errors.utils.isGhostError(err)) {
152
152
  return Promise.reject(err);
153
153
  }
154
154
  return Promise.reject(new errors.UnauthorizedError({err: err}));
@@ -59,7 +59,7 @@ async function setupUser(userData) {
59
59
  const owner = await models.User.findOne({role: 'Owner', status: 'all'});
60
60
 
61
61
  if (!owner) {
62
- throw new errors.GhostError({
62
+ throw new errors.InternalServerError({
63
63
  message: tpl(messages.setupUnableToRun)
64
64
  });
65
65
  }
@@ -12,7 +12,7 @@ module.exports = {
12
12
  !hasScheduled &&
13
13
  config.get('emailAnalytics') &&
14
14
  config.get('backgroundJobs:emailAnalytics') &&
15
- !process.env.NODE_ENV.match(/^testing/)
15
+ !process.env.NODE_ENV.startsWith('test')
16
16
  ) {
17
17
  // Don't register email analytics job if we have no emails,
18
18
  // processor usage from many sites spinning up threads can be high.
@@ -131,7 +131,7 @@ const transformEmailRecipientFilter = (emailRecipientFilter, {errorProperty = 'e
131
131
  // `paid` and `free` were swapped out for NQL filters in 4.5.0, we shouldn't see them here now
132
132
  case 'paid':
133
133
  case 'free':
134
- throw new errors.GhostError({
134
+ throw new errors.InternalServerError({
135
135
  message: tpl(messages.unexpectedFilterError, {
136
136
  property: errorProperty,
137
137
  value: emailRecipientFilter
@@ -140,7 +140,7 @@ const transformEmailRecipientFilter = (emailRecipientFilter, {errorProperty = 'e
140
140
  case 'all':
141
141
  return 'subscribed:true';
142
142
  case 'none':
143
- throw new errors.GhostError({
143
+ throw new errors.InternalServerError({
144
144
  message: tpl(messages.noneFilterError, {
145
145
  property: errorProperty
146
146
  })
@@ -352,7 +352,7 @@ async function sendEmailJob({emailModel, options}) {
352
352
  error: errorMessage
353
353
  }, {patch: true});
354
354
 
355
- throw new errors.GhostError({
355
+ throw new errors.InternalServerError({
356
356
  err: error,
357
357
  context: tpl(messages.sendEmailRequestFailed)
358
358
  });
@@ -416,7 +416,9 @@ function partitionMembersBySegment(memberRows, segments) {
416
416
  segmentedMemberRows = memberRows.filter(member => member.status !== 'free');
417
417
  memberRows = memberRows.filter(member => member.status === 'free');
418
418
  } else {
419
- throw new errors.ValidationError(tpl(messages.invalidSegment));
419
+ throw new errors.ValidationError({
420
+ message: tpl(messages.invalidSegment)
421
+ });
420
422
  }
421
423
 
422
424
  partitions[memberSegment] = segmentedMemberRows;
@@ -157,6 +157,16 @@ blockquote {
157
157
  letter-spacing: -0.2px;
158
158
  }
159
159
 
160
+ blockquote.kg-blockquote-alt {
161
+ border-left: 0 none;
162
+ padding-left: 50px;
163
+ padding-right: 50px;
164
+ margin-bottom: 2.5em;
165
+ text-align: center;
166
+ font-size: 1.2em;
167
+ color: #999999;
168
+ }
169
+
160
170
  blockquote p {
161
171
  margin: 0.8em 0;
162
172
  font-size: 1em;
@@ -557,52 +567,56 @@ figure blockquote p {
557
567
  padding-bottom: 4px;
558
568
  }
559
569
 
560
- .kg-card-callout {
570
+ .kg-callout-card {
561
571
  display: flex;
562
572
  margin: 0 0 1.5em 0;
563
573
  padding: 20px 28px;
564
574
  border-radius: 3px;
565
575
  }
566
576
 
567
- .kg-card-callout p {
577
+ .kg-callout-card p {
568
578
  margin: 0
569
579
  }
570
580
 
571
- .kg-card-callout-grey {
581
+ .kg-callout-card-grey {
572
582
  background: #eef0f2;
573
583
  }
574
584
 
575
- .kg-card-callout-white {
585
+ .kg-callout-card-white {
576
586
  background: #fff;
577
587
  box-shadow: inset 0 0 0 1px #dddedf;
578
588
  }
579
589
 
580
- .kg-card-callout-blue {
590
+ .kg-callout-card-blue {
581
591
  background: #E9F6FB;
582
592
  }
583
593
 
584
- .kg-card-callout-green {
594
+ .kg-callout-card-green {
585
595
  background: #E8F8EA;
586
596
  }
587
597
 
588
- .kg-card-callout-yellow {
598
+ .kg-callout-card-yellow {
589
599
  background: #FCF4E3;
590
600
  }
591
601
 
592
- .kg-card-callout-red {
602
+ .kg-callout-card-red {
593
603
  background: #FBE9E9;
594
604
  }
595
605
 
596
- .kg-card-callout-pink {
606
+ .kg-callout-card-pink {
597
607
  background: #FCEEF8;
598
608
  }
599
609
 
600
- .kg-card-callout-purple {
610
+ .kg-callout-card-purple {
601
611
  background: #F2EDFC;
602
612
  }
603
613
 
604
- .kg-card-callout-accent {
605
- background: var(--ghost-accent-color);
614
+ .kg-callout-card-accent {
615
+ background: ${templateSettings.accentColor || '#15212A'};
616
+ color: #fff;
617
+ }
618
+
619
+ .kg-callout-card-accent a {
606
620
  color: #fff;
607
621
  }
608
622
 
@@ -890,6 +904,11 @@ figure blockquote p {
890
904
  padding-left: 15px !important;
891
905
  }
892
906
 
907
+ table.body blockquote.kg-blockquote-alt {
908
+ padding-left: 50px !important;
909
+ margin-bottom: 2.5em !important;
910
+ }
911
+
893
912
  table.body blockquote + * {
894
913
  margin-top: 1.5em !important;
895
914
  }
@@ -6,6 +6,7 @@ const mail = require('../mail');
6
6
  const models = require('../../models');
7
7
  const signinEmail = require('./emails/signin');
8
8
  const signupEmail = require('./emails/signup');
9
+ const signupPaidEmail = require('./emails/signup-paid');
9
10
  const subscribeEmail = require('./emails/subscribe');
10
11
  const updateEmail = require('./emails/updateEmail');
11
12
  const SingleUseTokenProvider = require('./SingleUseTokenProvider');
@@ -49,6 +50,8 @@ function createApiInstance(config) {
49
50
  return `📫 Confirm your subscription to ${siteTitle}`;
50
51
  case 'signup':
51
52
  return `🙌 Complete your sign up to ${siteTitle}!`;
53
+ case 'signup-paid':
54
+ return `🙌 Thank you for signing up to ${siteTitle}!`;
52
55
  case 'updateEmail':
53
56
  return `📫 Confirm your email update for ${siteTitle}!`;
54
57
  case 'signin':
@@ -93,6 +96,23 @@ function createApiInstance(config) {
93
96
  Sent to ${email}
94
97
  If you did not make this request, you can simply delete this message. You will not be signed up, and no account will be created for you.
95
98
  `;
99
+ case 'signup-paid':
100
+ return `
101
+ Thank you for subscribing to ${siteTitle}!
102
+
103
+ Tap the link below to be automatically signed in:
104
+
105
+ ${url}
106
+
107
+ For your security, the link will expire in 24 hours time.
108
+
109
+ See you soon!
110
+
111
+ ---
112
+
113
+ Sent to ${email}
114
+ Thank you for subscribing to ${siteTitle}!
115
+ `;
96
116
  case 'updateEmail':
97
117
  return `
98
118
  Hey there,
@@ -139,6 +159,8 @@ function createApiInstance(config) {
139
159
  return subscribeEmail({url, email, siteTitle, accentColor, siteDomain, siteUrl});
140
160
  case 'signup':
141
161
  return signupEmail({url, email, siteTitle, accentColor, siteDomain, siteUrl});
162
+ case 'signup-paid':
163
+ return signupPaidEmail({url, email, siteTitle, accentColor, siteDomain, siteUrl});
142
164
  case 'updateEmail':
143
165
  return updateEmail({url, email, siteTitle, accentColor, siteDomain, siteUrl});
144
166
  case 'signin':
@@ -98,7 +98,7 @@ class MembersConfigProvider {
98
98
  */
99
99
  getStripeKeys(type) {
100
100
  if (type !== 'direct' && type !== 'connect') {
101
- throw new errors.IncorrectUsageError(tpl(messages.incorrectKeyType));
101
+ throw new errors.IncorrectUsageError({message: tpl(messages.incorrectKeyType)});
102
102
  }
103
103
 
104
104
  const secretKey = this._settingsCache.get(`stripe_${type === 'connect' ? 'connect_' : ''}secret_key`);
@@ -0,0 +1,168 @@
1
+ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, siteUrl}) => `
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>🙌 Thank you for signing up to ${siteTitle}!</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] .wrapper,
26
+ table[class=body] .article {
27
+ padding: 10px !important;
28
+ }
29
+ table[class=body] .content {
30
+ padding: 0 !important;
31
+ }
32
+ table[class=body] .container {
33
+ padding: 0 !important;
34
+ width: 100% !important;
35
+ }
36
+ table[class=body] .main {
37
+ border-left-width: 0 !important;
38
+ border-radius: 0 !important;
39
+ border-right-width: 0 !important;
40
+ }
41
+ table[class=body] .btn table {
42
+ width: 100% !important;
43
+ }
44
+ table[class=body] .btn a {
45
+ width: 100% !important;
46
+ }
47
+ table[class=body] .img-responsive {
48
+ height: auto !important;
49
+ max-width: 100% !important;
50
+ width: auto !important;
51
+ }
52
+ table[class=body] p[class=small],
53
+ table[class=body] a[class=small] {
54
+ font-size: 11px !important;
55
+ }
56
+ }
57
+ /* -------------------------------------
58
+ PRESERVE THESE STYLES IN THE HEAD
59
+ ------------------------------------- */
60
+ @media all {
61
+ .ExternalClass {
62
+ width: 100%;
63
+ }
64
+ .ExternalClass,
65
+ .ExternalClass p,
66
+ .ExternalClass span,
67
+ .ExternalClass font,
68
+ .ExternalClass td,
69
+ .ExternalClass div {
70
+ line-height: 100%;
71
+ }
72
+ .recipient-link a {
73
+ color: inherit !important;
74
+ font-family: inherit !important;
75
+ font-size: inherit !important;
76
+ font-weight: inherit !important;
77
+ line-height: inherit !important;
78
+ text-decoration: none !important;
79
+ }
80
+ #MessageViewBody a {
81
+ color: inherit;
82
+ text-decoration: none;
83
+ font-size: inherit;
84
+ font-family: inherit;
85
+ font-weight: inherit;
86
+ line-height: inherit;
87
+ }
88
+ }
89
+ hr {
90
+ border-width: 0;
91
+ height: 0;
92
+ margin-top: 34px;
93
+ margin-bottom: 34px;
94
+ border-bottom-width: 1px;
95
+ border-bottom-color: #EEF5F8;
96
+ }
97
+ a {
98
+ color: #3A464C;
99
+ }
100
+ </style>
101
+ </head>
102
+ <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%;">
103
+ <table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #FFFFFF;">
104
+ <tr>
105
+ <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;">&nbsp;</td>
106
+ <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;">
107
+ <div class="content" style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 600px; padding: 30px 20px;">
108
+
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;">Complete signup for ${siteTitle}!</span>
111
+ <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
112
+
113
+ <!-- START MAIN CONTENT AREA -->
114
+ <tr>
115
+ <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;">
116
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
117
+ <tr>
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
+ <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;">Thank you for subscribing to ${siteTitle}</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 be automatically signed in:</p>
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
+ <tbody>
123
+ <tr>
124
+ <td align="left" 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; padding-bottom: 35px;">
125
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
126
+ <tbody>
127
+ <tr>
128
+ <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; background-color: ${accentColor}; border-radius: 5px; text-align: center;"> <a href="${url}" target="_blank" style="display: inline-block; color: #ffffff; background-color: ${accentColor}; border: solid 1px ${accentColor}; 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: ${accentColor};">Confirm signup</a> </td>
129
+ </tr>
130
+ </tbody>
131
+ </table>
132
+ </td>
133
+ </tr>
134
+ </tbody>
135
+ </table>
136
+ <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: 25px;">For your security, the link will expire in 24 hours time.</p>
137
+ <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: 30px;">See you soon!</p>
138
+ <hr/>
139
+ <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
140
+ <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin-top: 0; color: #3A464C;">${url}</p>
141
+ </td>
142
+ </tr>
143
+ </table>
144
+ </td>
145
+ </tr>
146
+
147
+ <!-- START FOOTER -->
148
+ <tr>
149
+ <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-top: 2px;">
150
+ <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'; line-height: 18px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;">This message was sent from <a class="small" href="${siteUrl}" style="text-decoration: underline; color: #738A94; font-size: 11px;">${siteDomain}</a> to <a class="small" href="mailto:${email}" style="text-decoration: underline; color: #738A94; font-size: 11px;">${email}</a></p>
151
+ </td>
152
+ </tr>
153
+
154
+ <!-- END FOOTER -->
155
+
156
+ <!-- END MAIN CONTENT AREA -->
157
+ </table>
158
+
159
+ <!-- END CENTERED WHITE CONTAINER -->
160
+ </div>
161
+ </td>
162
+ <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;">&nbsp;</td>
163
+ </tr>
164
+ </table>
165
+ </body>
166
+ </html>
167
+ `;
168
+
@@ -159,12 +159,16 @@ const membersService = {
159
159
  }
160
160
 
161
161
  if (stripeService.api.configured && stripeService.api.mode === 'live') {
162
- throw new errors.IncorrectUsageError(tpl(messages.noLiveKeysInDevelopment));
162
+ throw new errors.IncorrectUsageError({
163
+ message: tpl(messages.noLiveKeysInDevelopment)
164
+ });
163
165
  }
164
166
  } else {
165
167
  const siteUrl = urlUtils.getSiteUrl();
166
168
  if (!/^https/.test(siteUrl) && stripeService.api.configured) {
167
- throw new errors.IncorrectUsageError(tpl(messages.sslRequiredForStripe));
169
+ throw new errors.IncorrectUsageError({
170
+ message: tpl(messages.sslRequiredForStripe)
171
+ });
168
172
  }
169
173
  }
170
174
  if (!membersApi) {
@@ -63,7 +63,7 @@ async function getStripeConnectTokenData(encodedData, getSessionProp) {
63
63
  const state = await getSessionProp(STATE_PROP);
64
64
 
65
65
  if (state !== data.s) {
66
- throw new errors.NoPermissionError(tpl(messages.incorrectState));
66
+ throw new errors.NoPermissionError({message: tpl(messages.incorrectState)});
67
67
  }
68
68
 
69
69
  return {
@@ -81,7 +81,9 @@ function checkCanConnect() {
81
81
  const siteUrlUsingSSL = /^https/.test(siteUrl);
82
82
  const cannotConnectToStripe = productionMode && !siteUrlUsingSSL;
83
83
  if (cannotConnectToStripe) {
84
- throw new errors.BadRequestError('Cannot connect to stripe unless site is using https://');
84
+ throw new errors.BadRequestError({
85
+ message: 'Cannot connect to stripe unless site is using https://'
86
+ });
85
87
  }
86
88
  }
87
89
 
@@ -35,8 +35,13 @@ class NFTOEmbedProvider {
35
35
  if (!match) {
36
36
  return null;
37
37
  }
38
+ const headers = {};
39
+ if (this.dependencies.config.apiKey) {
40
+ headers['X-API-KEY'] = this.dependencies.config.apiKey;
41
+ }
38
42
  const result = await externalRequest(`https://api.opensea.io/api/v1/asset/${transaction}/${asset}/?format=json`, {
39
- json: true
43
+ json: true,
44
+ headers
40
45
  });
41
46
  return {
42
47
  version: '1.0',
@@ -1,6 +1,7 @@
1
1
  const errors = require('@tryghost/errors');
2
2
  const tpl = require('@tryghost/tpl');
3
3
  const logging = require('@tryghost/logging');
4
+ const sentry = require('../../shared/sentry');
4
5
  const {extract, hasProvider} = require('oembed-parser');
5
6
  const cheerio = require('cheerio');
6
7
  const _ = require('lodash');
@@ -63,6 +64,7 @@ class OEmbed {
63
64
  */
64
65
  constructor({config, externalRequest}) {
65
66
  this.config = config;
67
+
66
68
  /** @type {IExternalRequest} */
67
69
  this.externalRequest = async (url, requestConfig) => {
68
70
  if (this.isIpOrLocalhost(url)) {
@@ -74,6 +76,7 @@ class OEmbed {
74
76
  }
75
77
  return response;
76
78
  };
79
+
77
80
  /** @type {ICustomProvider[]} */
78
81
  this.customProviders = [];
79
82
  }
@@ -108,27 +111,6 @@ class OEmbed {
108
111
  }
109
112
  }
110
113
 
111
- /**
112
- * @param {string} url
113
- */
114
- errorHandler(url) {
115
- /**
116
- * @param {Error|errors.GhostError} err
117
- */
118
- return async (err) => {
119
- // allow specific validation errors through for better error messages
120
- if (errors.utils.isIgnitionError(err) && err.errorType === 'ValidationError') {
121
- throw err;
122
- }
123
-
124
- // log the real error because we're going to throw a generic "Unknown provider" error
125
- logging.error(new errors.GhostError({err}));
126
-
127
- // default to unknown provider to avoid leaking any app specifics
128
- return this.unknownProvider(url);
129
- };
130
- }
131
-
132
114
  async fetchBookmarkData(url) {
133
115
  const metascraper = require('metascraper')([
134
116
  require('metascraper-url')(),
@@ -147,7 +129,14 @@ class OEmbed {
147
129
  const response = await this.externalRequest(url, {cookieJar});
148
130
 
149
131
  const html = response.body;
150
- scraperResponse = await metascraper({html, url});
132
+ try {
133
+ scraperResponse = await metascraper({html, url});
134
+ } catch (err) {
135
+ // Log to avoid being blind to errors happenning in metascraper
136
+ sentry.captureException(err);
137
+ logging.error(err);
138
+ return this.unknownProvider(url);
139
+ }
151
140
 
152
141
  const metadata = Object.assign({}, scraperResponse, {
153
142
  thumbnail: scraperResponse.image,
@@ -300,10 +289,13 @@ class OEmbed {
300
289
  * @returns {Promise<Object>}
301
290
  */
302
291
  async fetchOembedDataFromUrl(url, type) {
303
- let data;
304
-
305
292
  try {
306
293
  const urlObject = new URL(url);
294
+
295
+ // Trimming solves the difference of url validation between `new URL(url)`
296
+ // and metascraper.
297
+ url = url.trim();
298
+
307
299
  for (const provider of this.customProviders) {
308
300
  if (await provider.canSupportRequest(urlObject)) {
309
301
  const result = await provider.getOEmbedData(urlObject, this.externalRequest);
@@ -313,23 +305,39 @@ class OEmbed {
313
305
  }
314
306
  }
315
307
 
308
+ // fetch only bookmark when explicitly requested
316
309
  if (type === 'bookmark') {
317
310
  return this.fetchBookmarkData(url);
318
311
  }
319
312
 
320
- data = await this.fetchOembedData(url);
313
+ // attempt to fetch oembed
314
+ let data = await this.fetchOembedData(url);
321
315
 
316
+ // fallback to bookmark when we can't get oembed
322
317
  if (!data && !type) {
323
318
  data = await this.fetchBookmarkData(url);
324
319
  }
325
320
 
321
+ // couldn't get anything, throw a validation error
326
322
  if (!data) {
327
- data = await this.unknownProvider(url);
323
+ return this.unknownProvider(url);
328
324
  }
329
325
 
330
326
  return data;
331
- } catch (e) {
332
- return this.errorHandler(url);
327
+ } catch (err) {
328
+ // allow specific validation errors through for better error messages
329
+ if (errors.utils.isGhostError(err) && err.errorType === 'ValidationError') {
330
+ throw err;
331
+ }
332
+
333
+ // log the real error because we're going to throw a generic "Unknown provider" error
334
+ logging.error(new errors.InternalServerError({
335
+ message: 'Encountered error when fetching oembed',
336
+ err
337
+ }));
338
+
339
+ // default to unknown provider to avoid leaking any app specifics
340
+ return this.unknownProvider(url);
333
341
  }
334
342
  }
335
343
  }
@@ -128,7 +128,7 @@ CanThisResult.prototype.beginCheck = function (context) {
128
128
  context = parseContext(context);
129
129
 
130
130
  if (actionsMap.empty()) {
131
- throw new errors.GhostError({message: tpl(messages.noActionsMapFoundError)});
131
+ throw new errors.InternalServerError({message: tpl(messages.noActionsMapFoundError)});
132
132
  }
133
133
 
134
134
  // Kick off loading of user permissions if necessary