ghost 4.23.0 → 4.26.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.
Files changed (175) 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 +50 -215
  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 +20 -5
  15. package/core/boot.js +77 -36
  16. package/core/bridge.js +10 -10
  17. package/core/built/assets/ghost-dark-ef86e3bc7f0fb83d39d3d6a49bff8dd5.css +1 -0
  18. package/core/built/assets/ghost.min-57c1e677f42d596942d317ce93e8a62c.css +1 -0
  19. package/core/built/assets/{ghost.min-cccc107e881b74c7aaf1a73e1e5e0dee.js → ghost.min-f3c6886e191d34450e9ffca0c8fa056e.js} +543 -618
  20. package/core/built/assets/icons/audio-upload.svg +8 -0
  21. package/core/built/assets/{vendor.min-c9002845b6c30ac978abdadde9f33d7c.js → vendor.min-b6b8d2a31d61830c2d8f65c5ba54236a.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 +75 -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/audio.css +186 -0
  37. package/core/frontend/src/cards/css/blockquote.css +27 -0
  38. package/core/frontend/src/cards/css/bookmark.css +7 -0
  39. package/core/frontend/src/cards/css/button.css +4 -0
  40. package/core/frontend/src/cards/css/callout.css +23 -15
  41. package/core/frontend/src/cards/css/gallery.css +13 -3
  42. package/core/frontend/src/cards/css/toggle.css +48 -15
  43. package/core/frontend/src/cards/js/audio.js +137 -0
  44. package/core/frontend/web/middleware/error-handler.js +93 -0
  45. package/core/frontend/web/middleware/handle-image-sizes.js +3 -6
  46. package/core/frontend/web/middleware/index.js +1 -0
  47. package/core/frontend/web/middleware/serve-public-file.js +25 -8
  48. package/core/frontend/web/site.js +2 -5
  49. package/core/server/adapters/scheduling/SchedulingDefault.js +2 -2
  50. package/core/server/adapters/storage/LocalStorageBase.js +2 -2
  51. package/core/server/api/canary/db.js +2 -2
  52. package/core/server/api/canary/media.js +3 -2
  53. package/core/server/api/canary/oembed.js +16 -1
  54. package/core/server/api/canary/session.js +1 -1
  55. package/core/server/api/canary/slugs.js +1 -1
  56. package/core/server/api/canary/utils/permissions.js +2 -2
  57. package/core/server/api/canary/utils/serializers/output/config.js +2 -6
  58. package/core/server/api/v2/db.js +2 -2
  59. package/core/server/api/v2/session.js +1 -1
  60. package/core/server/api/v2/slugs.js +1 -1
  61. package/core/server/api/v2/utils/permissions.js +2 -2
  62. package/core/server/api/v3/db.js +2 -2
  63. package/core/server/api/v3/session.js +1 -1
  64. package/core/server/api/v3/slugs.js +1 -1
  65. package/core/server/api/v3/utils/permissions.js +2 -2
  66. package/core/server/data/db/state-manager.js +4 -4
  67. package/core/server/data/exporter/export-filename.js +1 -1
  68. package/core/server/data/importer/handlers/json.js +1 -1
  69. package/core/server/data/importer/import-manager.js +1 -1
  70. package/core/server/data/importer/importers/data/base.js +1 -1
  71. package/core/server/data/migrations/utils.js +2 -2
  72. package/core/server/data/migrations/versions/1.25/1-update-koenig-beta-html.js +1 -0
  73. package/core/server/data/migrations/versions/3.1/08-add-uuid-values-to-members.js +1 -0
  74. package/core/server/data/migrations/versions/3.22/02-settings-key-renames.js +2 -0
  75. package/core/server/data/migrations/versions/3.22/05-migrate-members-subscription-settings.js +3 -0
  76. package/core/server/data/migrations/versions/3.22/06-migrate-stripe-connect-settings.js +2 -0
  77. package/core/server/data/migrations/versions/3.23/01-migrate-bulk-email-settings.js +1 -0
  78. package/core/server/data/migrations/versions/3.29/01-remove-duplicate-subscriptions.js +2 -0
  79. package/core/server/data/migrations/versions/3.29/02-remove-duplicate-customers.js +2 -0
  80. package/core/server/data/migrations/versions/3.38/04-populate-recipient-filter-column.js +2 -0
  81. package/core/server/data/migrations/versions/4.0/01-update-mobiledoc.js +2 -0
  82. package/core/server/data/migrations/versions/4.0/03-populate-status-column-for-members.js +4 -0
  83. package/core/server/data/migrations/versions/4.0/06-populate-members-subscribe-events-table.js +1 -0
  84. package/core/server/data/migrations/versions/4.0/17-populate-members-status-events-table.js +1 -0
  85. package/core/server/data/migrations/versions/4.0/18-transform-urls-absolute-to-transform-ready.js +5 -0
  86. package/core/server/data/migrations/versions/4.0/22-solve-orphaned-webhooks.js +1 -0
  87. package/core/server/data/migrations/versions/4.0/23-regenerate-posts-html.js +1 -0
  88. package/core/server/data/migrations/versions/4.0/25-populate-members-paid-subscription-events-table.js +2 -1
  89. package/core/server/data/migrations/versions/4.12/02-fix-member-statuses.js +1 -0
  90. package/core/server/data/migrations/versions/4.14/01-fix-comped-member-statuses.js +3 -0
  91. package/core/server/data/migrations/versions/4.14/02-fix-free-members-status-events.js +1 -0
  92. package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +2 -0
  93. package/core/server/data/migrations/versions/4.23/01-truncate-offer-names.js +1 -0
  94. package/core/server/data/migrations/versions/4.3/04-attach-members-to-product.js +1 -0
  95. package/core/server/data/migrations/versions/4.4/01-restore-free-members-signup-setting-from-backup.js +1 -0
  96. package/core/server/data/migrations/versions/4.6/01-remove-comped-status.js +1 -0
  97. package/core/server/data/migrations/versions/4.8/04-migrate-show-newsletter-header-setting.js +1 -0
  98. package/core/server/data/migrations/versions/4.9/05-fix-missed-mobiledoc-url-transforms.js +1 -0
  99. package/core/server/data/migrations/versions/4.9/06-add-comped-status.js +1 -0
  100. package/core/server/data/migrations/versions/4.9/07-update-comped-members-status-events.js +1 -0
  101. package/core/server/data/schema/commands.js +2 -2
  102. package/core/server/ghost-server.js +2 -2
  103. package/core/server/lib/image/image-size.js +2 -2
  104. package/core/server/models/base/listeners.js +2 -2
  105. package/core/server/models/member-email-change-event.js +2 -2
  106. package/core/server/models/member-login-event.js +2 -2
  107. package/core/server/models/member-paid-subscription-event.js +3 -3
  108. package/core/server/models/member-payment-event.js +3 -3
  109. package/core/server/models/member-product-event.js +6 -6
  110. package/core/server/models/member-status-event.js +5 -3
  111. package/core/server/models/member-subscribe-event.js +9 -3
  112. package/core/server/models/relations/authors.js +1 -1
  113. package/core/server/models/settings.js +1 -1
  114. package/core/server/notify.js +1 -2
  115. package/core/server/services/auth/passwordreset.js +1 -1
  116. package/core/server/services/auth/setup.js +1 -1
  117. package/core/server/services/email-analytics/jobs/index.js +1 -1
  118. package/core/server/services/mega/mega.js +6 -4
  119. package/core/server/services/mega/template.js +47 -17
  120. package/core/server/services/members/api.js +22 -0
  121. package/core/server/services/members/config.js +1 -1
  122. package/core/server/services/members/emails/signup-paid.js +168 -0
  123. package/core/server/services/members/service.js +6 -2
  124. package/core/server/services/members/stripe-connect.js +4 -2
  125. package/core/server/services/nft-oembed.js +7 -2
  126. package/core/server/services/oembed.js +15 -3
  127. package/core/server/services/permissions/can-this.js +1 -1
  128. package/core/server/services/redirects/api.js +20 -25
  129. package/core/server/services/redirects/index.js +18 -10
  130. package/core/server/services/redirects/utils.js +14 -0
  131. package/core/server/services/redirects/validation.js +10 -0
  132. package/core/server/services/route-settings/default-settings-manager.js +1 -1
  133. package/core/server/services/route-settings/index.js +40 -17
  134. package/core/server/services/route-settings/route-settings.js +120 -115
  135. package/core/server/services/route-settings/settings-loader.js +18 -36
  136. package/core/server/services/route-settings/yaml-parser.js +1 -1
  137. package/core/server/services/slack.js +1 -1
  138. package/core/server/services/themes/activation-bridge.js +3 -3
  139. package/core/server/services/themes/storage.js +2 -2
  140. package/core/server/services/twitter-embed.js +81 -0
  141. package/core/server/services/url/LocalFileCache.js +75 -0
  142. package/core/server/services/url/UrlService.js +15 -47
  143. package/core/server/services/url/index.js +17 -4
  144. package/core/server/services/xmlrpc.js +2 -2
  145. package/core/server/web/admin/app.js +2 -5
  146. package/core/server/web/admin/controller.js +35 -12
  147. package/core/server/web/admin/middleware/redirect-admin-urls.js +15 -0
  148. package/core/server/web/admin/views/default-prod.html +4 -4
  149. package/core/server/web/admin/views/default.html +4 -4
  150. package/core/server/web/api/canary/admin/app.js +0 -3
  151. package/core/server/web/api/canary/admin/middleware.js +1 -1
  152. package/core/server/web/api/canary/content/app.js +0 -3
  153. package/core/server/web/api/v2/admin/app.js +0 -3
  154. package/core/server/web/api/v2/admin/middleware.js +1 -1
  155. package/core/server/web/api/v2/content/app.js +0 -3
  156. package/core/server/web/api/v3/admin/app.js +0 -3
  157. package/core/server/web/api/v3/admin/middleware.js +1 -1
  158. package/core/server/web/api/v3/content/app.js +0 -3
  159. package/core/server/web/members/app.js +0 -3
  160. package/core/server/web/oauth/app.js +0 -4
  161. package/core/server/web/parent/app.js +2 -13
  162. package/core/server/web/parent/backend.js +2 -0
  163. package/core/server/web/shared/middleware/error-handler.js +57 -162
  164. package/core/server/web/shared/middleware/index.js +0 -4
  165. package/core/shared/config/defaults.json +7 -1
  166. package/core/shared/express.js +1 -1
  167. package/core/shared/labs.js +12 -7
  168. package/core/shared/sentry.js +1 -1
  169. package/package.json +41 -40
  170. package/yarn.lock +799 -948
  171. package/content/themes/casper/assets/js/gallery-card.js +0 -24
  172. package/core/built/assets/ghost-dark-42cf6e0c730578940ec069bda45aea41.css +0 -1
  173. package/core/built/assets/ghost.min-fcf6a0738421f86c47c55f20d00c5ba9.css +0 -1
  174. package/core/frontend/services/theme-engine/middleware.js +0 -209
  175. package/core/server/web/shared/middleware/maintenance.js +0 -25
@@ -148,7 +148,7 @@ dd {
148
148
  }
149
149
 
150
150
  blockquote {
151
- margin: 2em 0;
151
+ margin: 2em 0 2em 0;
152
152
  padding: 0 25px 0 25px;
153
153
  border-left: ${templateSettings.accentColor || '#15212A'} 2px solid;
154
154
  font-size: 17px;
@@ -157,6 +157,15 @@ blockquote {
157
157
  letter-spacing: -0.2px;
158
158
  }
159
159
 
160
+ blockquote.kg-blockquote-alt {
161
+ border-left: 0 none;
162
+ padding: 0 50px 0 50px;
163
+ text-align: center;
164
+ font-size: 1.2em;
165
+ font-style: italic;
166
+ color: #999999;
167
+ }
168
+
160
169
  blockquote p {
161
170
  margin: 0.8em 0;
162
171
  font-size: 1em;
@@ -557,52 +566,66 @@ figure blockquote p {
557
566
  padding-bottom: 4px;
558
567
  }
559
568
 
560
- .kg-card-callout {
569
+ .kg-twitter-link {
570
+ display: block;
571
+ text-decoration: none !important;
572
+ color: #15212A !important;
573
+ font-family: inherit !important;
574
+ font-size: 15px;
575
+ padding: 8px;
576
+ line-height: 1.3em;
577
+ }
578
+
579
+ .kg-callout-card {
561
580
  display: flex;
562
581
  margin: 0 0 1.5em 0;
563
582
  padding: 20px 28px;
564
583
  border-radius: 3px;
565
584
  }
566
585
 
567
- .kg-card-callout p {
586
+ .kg-callout-card p {
568
587
  margin: 0
569
588
  }
570
589
 
571
- .kg-card-callout-grey {
590
+ .kg-callout-card-grey {
572
591
  background: #eef0f2;
573
592
  }
574
593
 
575
- .kg-card-callout-white {
594
+ .kg-callout-card-white {
576
595
  background: #fff;
577
596
  box-shadow: inset 0 0 0 1px #dddedf;
578
597
  }
579
598
 
580
- .kg-card-callout-blue {
599
+ .kg-callout-card-blue {
581
600
  background: #E9F6FB;
582
601
  }
583
602
 
584
- .kg-card-callout-green {
603
+ .kg-callout-card-green {
585
604
  background: #E8F8EA;
586
605
  }
587
606
 
588
- .kg-card-callout-yellow {
607
+ .kg-callout-card-yellow {
589
608
  background: #FCF4E3;
590
609
  }
591
610
 
592
- .kg-card-callout-red {
611
+ .kg-callout-card-red {
593
612
  background: #FBE9E9;
594
613
  }
595
614
 
596
- .kg-card-callout-pink {
615
+ .kg-callout-card-pink {
597
616
  background: #FCEEF8;
598
617
  }
599
618
 
600
- .kg-card-callout-purple {
619
+ .kg-callout-card-purple {
601
620
  background: #F2EDFC;
602
621
  }
603
622
 
604
- .kg-card-callout-accent {
605
- background: var(--ghost-accent-color);
623
+ .kg-callout-card-accent {
624
+ background: ${templateSettings.accentColor || '#15212A'};
625
+ color: #fff;
626
+ }
627
+
628
+ .kg-callout-card-accent a {
606
629
  color: #fff;
607
630
  }
608
631
 
@@ -884,10 +907,17 @@ figure blockquote p {
884
907
  }
885
908
 
886
909
  table.body blockquote {
887
- font-size: 17px !important;
888
- line-height: 1.6em !important;
889
- margin-bottom: 0 !important;
890
- padding-left: 15px !important;
910
+ font-size: 17px;
911
+ line-height: 1.6em;
912
+ margin-bottom: 0;
913
+ padding-left: 15px;
914
+ }
915
+
916
+ table.body blockquote.kg-blockquote-alt {
917
+ border-left: 0 none !important;
918
+ margin: 0 0 2.5em 0;
919
+ padding: 0 50px 0 50px;
920
+ font-size: 1.2em;
891
921
  }
892
922
 
893
923
  table.body blockquote + * {
@@ -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
+ Hey there!
102
+
103
+ Thank you for subscribing to ${siteTitle}. 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;">Thank you for subscribing to ${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;">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;">Thank you for subscribing to ${siteTitle}. 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};">Sign in</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,13 +35,18 @@ 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',
43
48
  type: 'nft',
44
- title: result.body.name,
49
+ title: result.body.name ? result.body.name : `#${result.body.token_id}`,
45
50
  author_name: result.body.creator.user.username,
46
51
  author_url: `https://opensea.io/${result.body.creator.user.username}`,
47
52
  provider_name: 'OpenSea',
@@ -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');
@@ -128,7 +129,14 @@ class OEmbed {
128
129
  const response = await this.externalRequest(url, {cookieJar});
129
130
 
130
131
  const html = response.body;
131
- 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
+ }
132
140
 
133
141
  const metadata = Object.assign({}, scraperResponse, {
134
142
  thumbnail: scraperResponse.image,
@@ -284,6 +292,10 @@ class OEmbed {
284
292
  try {
285
293
  const urlObject = new URL(url);
286
294
 
295
+ // Trimming solves the difference of url validation between `new URL(url)`
296
+ // and metascraper.
297
+ url = url.trim();
298
+
287
299
  for (const provider of this.customProviders) {
288
300
  if (await provider.canSupportRequest(urlObject)) {
289
301
  const result = await provider.getOEmbedData(urlObject, this.externalRequest);
@@ -314,12 +326,12 @@ class OEmbed {
314
326
  return data;
315
327
  } catch (err) {
316
328
  // allow specific validation errors through for better error messages
317
- if (errors.utils.isIgnitionError(err) && err.errorType === 'ValidationError') {
329
+ if (errors.utils.isGhostError(err) && err.errorType === 'ValidationError') {
318
330
  throw err;
319
331
  }
320
332
 
321
333
  // log the real error because we're going to throw a generic "Unknown provider" error
322
- logging.error(new errors.GhostError({
334
+ logging.error(new errors.InternalServerError({
323
335
  message: 'Encountered error when fetching oembed',
324
336
  err
325
337
  }));
@@ -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
@@ -1,14 +1,11 @@
1
1
  const fs = require('fs-extra');
2
2
  const path = require('path');
3
- const moment = require('moment-timezone');
4
3
  const yaml = require('js-yaml');
5
4
 
6
5
  const logging = require('@tryghost/logging');
7
6
  const tpl = require('@tryghost/tpl');
8
7
  const errors = require('@tryghost/errors');
9
8
 
10
- const validation = require('./validation');
11
-
12
9
  const messages = {
13
10
  jsonParse: 'Could not parse JSON: {context}.',
14
11
  yamlParse: 'Could not parse YAML: {context}.',
@@ -22,7 +19,7 @@ const messages = {
22
19
  * @typedef {Object} RedirectConfig
23
20
  * @property {String} from - Defines the relative incoming URL or pattern (regex)
24
21
  * @property {String} to - Defines where the incoming traffic should be redirected to, which can be a static URL, or a dynamic value using regex (example: "to": "/$1/")
25
- * @property {boolean} permanent - Can be defined with true for a permanent HTTP 301 redirect, or false for a temporary HTTP 302 redirect
22
+ * @property {boolean} [permanent] - Can be defined with true for a permanent HTTP 301 redirect, or false for a temporary HTTP 302 redirect
26
23
  */
27
24
 
28
25
  /**
@@ -37,7 +34,7 @@ const readRedirectsFile = async (redirectsPath) => {
37
34
  return '';
38
35
  }
39
36
 
40
- if (errors.utils.isIgnitionError(err)) {
37
+ if (errors.utils.isGhostError(err)) {
41
38
  throw err;
42
39
  }
43
40
 
@@ -120,16 +117,6 @@ const parseRedirectsFile = (content, ext) => {
120
117
  throw new errors.IncorrectUsageError();
121
118
  };
122
119
 
123
- /**
124
- * @param {string} filePath
125
- * @returns {string}
126
- */
127
- const getBackupRedirectsFilePath = (filePath) => {
128
- const {dir, name, ext} = path.parse(filePath);
129
-
130
- return path.join(dir, `${name}-${moment().format('YYYY-MM-DD-HH-mm-ss')}${ext}`);
131
- };
132
-
133
120
  /**
134
121
  * @typedef {object} IRedirectManager
135
122
  */
@@ -138,14 +125,22 @@ class CustomRedirectsAPI {
138
125
  /**
139
126
  * @param {object} config
140
127
  * @param {string} config.basePath
141
- *
142
- * @param {IRedirectManager} redirectManager
128
+ * @param {Function} config.validate - validates redirects configuration
129
+ * @param {Function} config.getBackupFilePath
130
+ * @param {IRedirectManager} config.redirectManager
143
131
  */
144
- constructor(config, redirectManager) {
132
+ constructor({basePath, validate, redirectManager, getBackupFilePath}) {
145
133
  /** @private */
146
- this.config = config;
134
+ this.basePath = basePath;
135
+
147
136
  /** @private */
148
137
  this.redirectManager = redirectManager;
138
+
139
+ /** @private */
140
+ this.validate = validate;
141
+
142
+ /** @private */
143
+ this.getBackupFilePath = getBackupFilePath;
149
144
  }
150
145
 
151
146
  async init() {
@@ -159,7 +154,7 @@ class CustomRedirectsAPI {
159
154
  const content = await readRedirectsFile(filePath);
160
155
  const ext = path.extname(filePath);
161
156
  const redirects = parseRedirectsFile(content, ext);
162
- validation.validate(redirects);
157
+ this.validate(redirects);
163
158
 
164
159
  this.redirectManager.removeAllRedirects();
165
160
  for (const redirect of redirects) {
@@ -167,7 +162,7 @@ class CustomRedirectsAPI {
167
162
  }
168
163
  }
169
164
  } catch (err) {
170
- if (errors.utils.isIgnitionError(err)) {
165
+ if (errors.utils.isGhostError(err)) {
171
166
  logging.error(err);
172
167
  } else {
173
168
  logging.error(new errors.IncorrectUsageError({
@@ -187,7 +182,7 @@ class CustomRedirectsAPI {
187
182
  * @returns {string}
188
183
  */
189
184
  createRedirectsFilePath(ext) {
190
- return path.join(this.config.basePath, `redirects${ext}`);
185
+ return path.join(this.basePath, `redirects${ext}`);
191
186
  }
192
187
 
193
188
  /**
@@ -222,7 +217,7 @@ class CustomRedirectsAPI {
222
217
  const redirectsFilePath = await this.getRedirectsFilePath();
223
218
 
224
219
  if (redirectsFilePath) {
225
- const backupRedirectsPath = getBackupRedirectsFilePath(redirectsFilePath);
220
+ const backupRedirectsPath = this.getBackupFilePath(redirectsFilePath);
226
221
 
227
222
  const backupExists = await fs.pathExists(backupRedirectsPath);
228
223
  if (backupExists) {
@@ -234,10 +229,10 @@ class CustomRedirectsAPI {
234
229
 
235
230
  const content = await readRedirectsFile(filePath);
236
231
  const parsed = parseRedirectsFile(content, ext);
237
- validation.validate(parsed);
232
+ this.validate(parsed);
238
233
 
239
234
  if (ext === '.json') {
240
- await fs.writeFile(this.createRedirectsFilePath('.json'), JSON.stringify(content), 'utf-8');
235
+ await fs.writeFile(this.createRedirectsFilePath('.json'), JSON.stringify(parsed), 'utf-8');
241
236
  } else if (ext === '.yaml') {
242
237
  await fs.copy(filePath, this.createRedirectsFilePath('.yaml'));
243
238
  }
@@ -3,21 +3,27 @@ const urlUtils = require('../../../shared/url-utils');
3
3
 
4
4
  const DynamicRedirectManager = require('@tryghost/express-dynamic-redirects');
5
5
  const CustomRedirectsAPI = require('./api');
6
-
7
- const redirectManager = new DynamicRedirectManager({
8
- permanentMaxAge: config.get('caching:customRedirects:maxAge'),
9
- getSubdirectoryURL: (pathname) => {
10
- return urlUtils.urlJoin(urlUtils.getSubdir(), pathname);
11
- }
12
- });
6
+ const validation = require('./validation');
7
+ const {getBackupRedirectsFilePath} = require('./utils');
13
8
 
14
9
  let customRedirectsAPI;
10
+ let redirectManager;
15
11
 
16
12
  module.exports = {
17
13
  init() {
14
+ redirectManager = new DynamicRedirectManager({
15
+ permanentMaxAge: config.get('caching:customRedirects:maxAge'),
16
+ getSubdirectoryURL: (pathname) => {
17
+ return urlUtils.urlJoin(urlUtils.getSubdir(), pathname);
18
+ }
19
+ });
20
+
18
21
  customRedirectsAPI = new CustomRedirectsAPI({
19
- basePath: config.getContentPath('data')
20
- }, redirectManager);
22
+ basePath: config.getContentPath('data'),
23
+ redirectManager,
24
+ getBackupFilePath: getBackupRedirectsFilePath,
25
+ validate: validation.validate.bind(validation)
26
+ });
21
27
 
22
28
  return customRedirectsAPI.init();
23
29
  },
@@ -26,5 +32,7 @@ module.exports = {
26
32
  return customRedirectsAPI;
27
33
  },
28
34
 
29
- middleware: redirectManager.handleRequest
35
+ get middleware() {
36
+ return redirectManager.handleRequest;
37
+ }
30
38
  };
@@ -0,0 +1,14 @@
1
+ const path = require('path');
2
+ const moment = require('moment-timezone');
3
+
4
+ /**
5
+ * @param {string} filePath
6
+ * @returns {string}
7
+ */
8
+ const getBackupRedirectsFilePath = (filePath) => {
9
+ const {dir, name, ext} = path.parse(filePath);
10
+
11
+ return path.join(dir, `${name}-${moment().format('YYYY-MM-DD-HH-mm-ss')}${ext}`);
12
+ };
13
+
14
+ module.exports.getBackupRedirectsFilePath = getBackupRedirectsFilePath;