ghost 4.24.0 → 4.25.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 (137) 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/boot.js +2 -2
  15. package/core/built/assets/ghost-dark-d690e732e17ffc794e2e59c1467ca282.css +1 -0
  16. package/core/built/assets/ghost.min-043bb7480a0810109b130f13b2a4235e.css +1 -0
  17. package/core/built/assets/{ghost.min-d5595f9c71ebc534ccf9ac78483d357c.js → ghost.min-bc72f685c1c9adc9885925c1412435a5.js} +502 -577
  18. package/core/built/assets/icons/audio-upload.svg +8 -0
  19. package/core/built/assets/{vendor.min-1a84ac3ef74edf31c6e86810b45221cc.js → vendor.min-d1234c632a54502777c34e50752fa3fc.js} +2598 -2137
  20. package/core/frontend/apps/amp/lib/helpers/amp_content.js +2 -2
  21. package/core/frontend/apps/amp/lib/views/amp.hbs +8 -0
  22. package/core/frontend/apps/private-blogging/index.js +1 -1
  23. package/core/frontend/services/apps/index.js +1 -1
  24. package/core/frontend/services/apps/loader.js +3 -3
  25. package/core/frontend/services/helpers/handlebars.js +1 -1
  26. package/core/frontend/services/theme-engine/middleware/ensure-active-theme.js +34 -0
  27. package/core/frontend/services/theme-engine/middleware/index.js +6 -0
  28. package/core/frontend/services/theme-engine/middleware/update-global-template-options.js +116 -0
  29. package/core/frontend/services/theme-engine/middleware/update-local-template-data.js +9 -0
  30. package/core/frontend/services/theme-engine/middleware/update-local-template-options.js +57 -0
  31. package/core/frontend/src/cards/css/button.css +4 -0
  32. package/core/frontend/src/cards/css/callout.css +10 -10
  33. package/core/frontend/web/middleware/error-handler.js +93 -0
  34. package/core/frontend/web/middleware/handle-image-sizes.js +3 -6
  35. package/core/frontend/web/middleware/index.js +1 -0
  36. package/core/frontend/web/site.js +1 -1
  37. package/core/server/adapters/scheduling/SchedulingDefault.js +2 -2
  38. package/core/server/adapters/storage/LocalStorageBase.js +2 -2
  39. package/core/server/api/canary/db.js +2 -2
  40. package/core/server/api/canary/media.js +3 -2
  41. package/core/server/api/canary/oembed.js +16 -1
  42. package/core/server/api/canary/session.js +1 -1
  43. package/core/server/api/canary/slugs.js +1 -1
  44. package/core/server/api/canary/utils/permissions.js +2 -2
  45. package/core/server/api/canary/utils/serializers/output/config.js +2 -6
  46. package/core/server/api/v2/db.js +2 -2
  47. package/core/server/api/v2/session.js +1 -1
  48. package/core/server/api/v2/slugs.js +1 -1
  49. package/core/server/api/v2/utils/permissions.js +2 -2
  50. package/core/server/api/v3/db.js +2 -2
  51. package/core/server/api/v3/session.js +1 -1
  52. package/core/server/api/v3/slugs.js +1 -1
  53. package/core/server/api/v3/utils/permissions.js +2 -2
  54. package/core/server/data/db/state-manager.js +4 -4
  55. package/core/server/data/exporter/export-filename.js +1 -1
  56. package/core/server/data/importer/handlers/json.js +1 -1
  57. package/core/server/data/importer/import-manager.js +1 -1
  58. package/core/server/data/importer/importers/data/base.js +1 -1
  59. package/core/server/data/migrations/utils.js +2 -2
  60. package/core/server/data/migrations/versions/1.25/1-update-koenig-beta-html.js +1 -0
  61. package/core/server/data/migrations/versions/3.1/08-add-uuid-values-to-members.js +1 -0
  62. package/core/server/data/migrations/versions/3.22/02-settings-key-renames.js +2 -0
  63. package/core/server/data/migrations/versions/3.22/05-migrate-members-subscription-settings.js +3 -0
  64. package/core/server/data/migrations/versions/3.22/06-migrate-stripe-connect-settings.js +2 -0
  65. package/core/server/data/migrations/versions/3.23/01-migrate-bulk-email-settings.js +1 -0
  66. package/core/server/data/migrations/versions/3.29/01-remove-duplicate-subscriptions.js +2 -0
  67. package/core/server/data/migrations/versions/3.29/02-remove-duplicate-customers.js +2 -0
  68. package/core/server/data/migrations/versions/3.38/04-populate-recipient-filter-column.js +2 -0
  69. package/core/server/data/migrations/versions/4.0/01-update-mobiledoc.js +2 -0
  70. package/core/server/data/migrations/versions/4.0/03-populate-status-column-for-members.js +4 -0
  71. package/core/server/data/migrations/versions/4.0/06-populate-members-subscribe-events-table.js +1 -0
  72. package/core/server/data/migrations/versions/4.0/17-populate-members-status-events-table.js +1 -0
  73. package/core/server/data/migrations/versions/4.0/18-transform-urls-absolute-to-transform-ready.js +5 -0
  74. package/core/server/data/migrations/versions/4.0/22-solve-orphaned-webhooks.js +1 -0
  75. package/core/server/data/migrations/versions/4.0/23-regenerate-posts-html.js +1 -0
  76. package/core/server/data/migrations/versions/4.0/25-populate-members-paid-subscription-events-table.js +2 -1
  77. package/core/server/data/migrations/versions/4.12/02-fix-member-statuses.js +1 -0
  78. package/core/server/data/migrations/versions/4.14/01-fix-comped-member-statuses.js +3 -0
  79. package/core/server/data/migrations/versions/4.14/02-fix-free-members-status-events.js +1 -0
  80. package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +2 -0
  81. package/core/server/data/migrations/versions/4.23/01-truncate-offer-names.js +1 -0
  82. package/core/server/data/migrations/versions/4.3/04-attach-members-to-product.js +1 -0
  83. package/core/server/data/migrations/versions/4.4/01-restore-free-members-signup-setting-from-backup.js +1 -0
  84. package/core/server/data/migrations/versions/4.6/01-remove-comped-status.js +1 -0
  85. package/core/server/data/migrations/versions/4.8/04-migrate-show-newsletter-header-setting.js +1 -0
  86. package/core/server/data/migrations/versions/4.9/05-fix-missed-mobiledoc-url-transforms.js +1 -0
  87. package/core/server/data/migrations/versions/4.9/06-add-comped-status.js +1 -0
  88. package/core/server/data/migrations/versions/4.9/07-update-comped-members-status-events.js +1 -0
  89. package/core/server/data/schema/commands.js +2 -2
  90. package/core/server/ghost-server.js +2 -2
  91. package/core/server/lib/image/image-size.js +2 -2
  92. package/core/server/models/base/listeners.js +2 -2
  93. package/core/server/models/member-email-change-event.js +2 -2
  94. package/core/server/models/member-login-event.js +2 -2
  95. package/core/server/models/member-paid-subscription-event.js +3 -3
  96. package/core/server/models/member-payment-event.js +3 -3
  97. package/core/server/models/member-product-event.js +6 -6
  98. package/core/server/models/member-status-event.js +5 -3
  99. package/core/server/models/member-subscribe-event.js +9 -3
  100. package/core/server/models/relations/authors.js +1 -1
  101. package/core/server/models/settings.js +1 -1
  102. package/core/server/services/auth/passwordreset.js +1 -1
  103. package/core/server/services/auth/setup.js +1 -1
  104. package/core/server/services/mega/mega.js +6 -4
  105. package/core/server/services/mega/template.js +12 -12
  106. package/core/server/services/members/api.js +22 -0
  107. package/core/server/services/members/config.js +1 -1
  108. package/core/server/services/members/emails/signup-paid.js +168 -0
  109. package/core/server/services/members/service.js +6 -2
  110. package/core/server/services/members/stripe-connect.js +4 -2
  111. package/core/server/services/nft-oembed.js +6 -1
  112. package/core/server/services/oembed.js +6 -2
  113. package/core/server/services/permissions/can-this.js +1 -1
  114. package/core/server/services/redirects/api.js +2 -2
  115. package/core/server/services/route-settings/default-settings-manager.js +1 -1
  116. package/core/server/services/route-settings/route-settings.js +4 -12
  117. package/core/server/services/route-settings/settings-loader.js +4 -4
  118. package/core/server/services/route-settings/yaml-parser.js +1 -1
  119. package/core/server/services/slack.js +1 -1
  120. package/core/server/services/themes/storage.js +2 -2
  121. package/core/server/services/twitter-embed.js +80 -0
  122. package/core/server/services/xmlrpc.js +2 -2
  123. package/core/server/web/admin/views/default-prod.html +4 -4
  124. package/core/server/web/admin/views/default.html +4 -4
  125. package/core/server/web/api/canary/admin/middleware.js +1 -1
  126. package/core/server/web/api/v2/admin/middleware.js +1 -1
  127. package/core/server/web/api/v3/admin/middleware.js +1 -1
  128. package/core/server/web/shared/middleware/error-handler.js +28 -150
  129. package/core/shared/config/defaults.json +7 -1
  130. package/core/shared/labs.js +3 -4
  131. package/core/shared/sentry.js +1 -1
  132. package/package.json +11 -11
  133. package/yarn.lock +426 -731
  134. package/content/themes/casper/assets/js/gallery-card.js +0 -24
  135. package/core/built/assets/ghost-dark-e7b57ab951512c5719aee89b16b9a448.css +0 -1
  136. package/core/built/assets/ghost.min-7f3603dbeb5ebf0ec09e207ae82fb4e3.css +0 -1
  137. package/core/frontend/services/theme-engine/middleware.js +0 -209
@@ -143,7 +143,7 @@ function getAmperizeHTML(html, post) {
143
143
  if (err) {
144
144
  if (err.src) {
145
145
  // This is a valid 500 GhostError because it means the amperize parser is unable to handle some Ghost HTML.
146
- logging.error(new errors.GhostError({
146
+ logging.error(new errors.InternalServerError({
147
147
  message: `AMP HTML couldn't be parsed: ${err.src}`,
148
148
  code: 'AMP_PARSER_ERROR',
149
149
  err: err,
@@ -151,7 +151,7 @@ function getAmperizeHTML(html, post) {
151
151
  help: 'Please share this error on GitHub or https://forum.ghost.org'
152
152
  }));
153
153
  } else {
154
- logging.error(new errors.GhostError({err, code: 'AMP_PARSER_ERROR'}));
154
+ logging.error(new errors.InternalServerError({err, code: 'AMP_PARSER_ERROR'}));
155
155
  }
156
156
 
157
157
  // save it in cache to prevent multiple calls to Amperize until
@@ -457,6 +457,14 @@
457
457
  margin: 0 .5em;
458
458
  }
459
459
 
460
+ .kg-toggle-card-icon {
461
+ display: none;
462
+ }
463
+
464
+ .kg-toggle-content {
465
+ margin-top: 0.8rem;
466
+ }
467
+
460
468
  .kg-nft-card-container {
461
469
  position: relative;
462
470
  display: flex;
@@ -24,7 +24,7 @@ let checkSubdir = function checkSubdir() {
24
24
  paths = urlUtils.getSubdir().split('/');
25
25
 
26
26
  if (paths.pop() === PRIVATE_KEYWORD) {
27
- logging.error(new errors.GhostError({
27
+ logging.error(new errors.InternalServerError({
28
28
  message: tpl(messages.urlCannotContainPrivateSubdir.error),
29
29
  context: tpl(messages.urlCannotContainPrivateSubdir.description),
30
30
  help: tpl(messages.urlCannotContainPrivateSubdir.help)
@@ -18,7 +18,7 @@ module.exports = {
18
18
 
19
19
  return Promise.map(appsToLoad, appName => loader.activateAppByName(appName))
20
20
  .catch(function (err) {
21
- logging.error(new errors.GhostError({
21
+ logging.error(new errors.InternalServerError({
22
22
  err: err,
23
23
  context: tpl(messages.appWillNotBeLoadedError),
24
24
  help: tpl(messages.appWillNotBeLoadedHelp)
@@ -40,9 +40,9 @@ module.exports = {
40
40
 
41
41
  // Check for an activate() method on the app.
42
42
  if (!_.isFunction(app.activate)) {
43
- return Promise.reject(new errors.IncorrectUsageError(
44
- tpl(messages.noActivateMethodLoadingAppError, {name: name})
45
- ));
43
+ return Promise.reject(new errors.IncorrectUsageError({
44
+ message: tpl(messages.noActivateMethodLoadingAppError, {name: name})
45
+ }));
46
46
  }
47
47
 
48
48
  // Wrapping the activate() with a when because it's possible
@@ -17,7 +17,7 @@ function asyncHelperWrapper(hbsInstance, name, fn) {
17
17
  Promise.resolve(fn.call(this, context, options)).then(function asyncHelperSuccess(result) {
18
18
  cb(result);
19
19
  }).catch(function asyncHelperError(err) {
20
- const wrappedErr = err instanceof errors.GhostError ? err : new errors.IncorrectUsageError({
20
+ const wrappedErr = errors.utils.isGhostError(err) ? err : new errors.IncorrectUsageError({
21
21
  err: err,
22
22
  context: 'registerAsyncThemeHelper: ' + name,
23
23
  errorDetails: {
@@ -0,0 +1,34 @@
1
+ const errors = require('@tryghost/errors');
2
+ const tpl = require('@tryghost/tpl');
3
+
4
+ const activeTheme = require('../active');
5
+ const settingsCache = require('../../../../shared/settings-cache');
6
+
7
+ const messages = {
8
+ missingTheme: 'The currently active theme "{theme}" is missing.'
9
+ };
10
+
11
+ // ### Ensure Active Theme
12
+ // Ensure there's a properly set & mounted active theme before attempting to serve a site request
13
+ // If there is no active theme, throw an error
14
+ // Else, ensure the active theme is mounted
15
+ function ensureActiveTheme(req, res, next) {
16
+ // CASE: this means that the theme hasn't been loaded yet i.e. there is no active theme
17
+ if (!activeTheme.get()) {
18
+ // This is the one place we ACTUALLY throw an error for a missing theme as it's a request we cannot serve
19
+ return next(new errors.InternalServerError({
20
+ // We use the settingsCache here, because the setting will be set,
21
+ // even if the theme itself is not usable because it is invalid or missing.
22
+ message: tpl(messages.missingTheme, {theme: settingsCache.get('active_theme')})
23
+ }));
24
+ }
25
+
26
+ // If the active theme has not yet been mounted, mount it into express
27
+ if (!activeTheme.get().mounted) {
28
+ activeTheme.get().mount(req.app);
29
+ }
30
+
31
+ next();
32
+ }
33
+
34
+ module.exports = ensureActiveTheme;
@@ -0,0 +1,6 @@
1
+ module.exports = [
2
+ require('./ensure-active-theme'),
3
+ require('./update-global-template-options'),
4
+ require('./update-local-template-data'),
5
+ require('./update-local-template-options')
6
+ ];
@@ -0,0 +1,116 @@
1
+ const hbs = require('../engine');
2
+ const urlUtils = require('../../../../shared/url-utils');
3
+ const {api} = require('../../proxy');
4
+ const settingsCache = require('../../../../shared/settings-cache');
5
+ const customThemeSettingsCache = require('../../../../shared/custom-theme-settings-cache');
6
+ const labs = require('../../../../shared/labs');
7
+ const activeTheme = require('../active');
8
+
9
+ function calculateLegacyPriceData(products) {
10
+ const defaultPrice = {
11
+ amount: 0,
12
+ currency: 'usd',
13
+ interval: 'year',
14
+ nickname: ''
15
+ };
16
+
17
+ function makePriceObject(price) {
18
+ const numberAmount = 0 + price.amount;
19
+ const dollarAmount = numberAmount ? Math.round(numberAmount / 100) : 0;
20
+ return {
21
+ valueOf() {
22
+ return dollarAmount;
23
+ },
24
+ amount: numberAmount,
25
+ currency: price.currency,
26
+ nickname: price.name,
27
+ interval: price.interval
28
+ };
29
+ }
30
+
31
+ const defaultProduct = products[0] || {};
32
+
33
+ const monthlyPrice = makePriceObject(defaultProduct.monthly_price || defaultPrice);
34
+
35
+ const yearlyPrice = makePriceObject(defaultProduct.yearly_price || defaultPrice);
36
+
37
+ const priceData = {
38
+ monthly: monthlyPrice,
39
+ yearly: yearlyPrice,
40
+ currency: monthlyPrice ? monthlyPrice.currency : defaultPrice.currency
41
+ };
42
+
43
+ return priceData;
44
+ }
45
+
46
+ async function getProductAndPricesData() {
47
+ try {
48
+ const page = await api.canary.productsPublic.browse({
49
+ include: ['monthly_price', 'yearly_price'],
50
+ limit: 'all'
51
+ });
52
+
53
+ return page.products;
54
+ } catch (err) {
55
+ return [];
56
+ }
57
+ }
58
+
59
+ function getSiteData() {
60
+ let siteData = settingsCache.getPublic();
61
+
62
+ // theme-only computed property added to @site
63
+ if (settingsCache.get('members_signup_access') === 'none') {
64
+ const escapedUrl = encodeURIComponent(urlUtils.urlFor({relativeUrl: '/rss/'}, true));
65
+ siteData.signup_url = `https://feedly.com/i/subscription/feed/${escapedUrl}`;
66
+ } else {
67
+ siteData.signup_url = '#/portal';
68
+ }
69
+
70
+ return siteData;
71
+ }
72
+
73
+ async function updateGlobalTemplateOptions(req, res, next) {
74
+ // Static information, same for every request unless the settings change
75
+ // @TODO: bind this once and then update based on events?
76
+ // @TODO: decouple theme layer from settings cache using the Content API
77
+ const siteData = getSiteData();
78
+ const labsData = labs.getAll();
79
+
80
+ const themeData = {
81
+ posts_per_page: activeTheme.get().config('posts_per_page'),
82
+ image_sizes: activeTheme.get().config('image_sizes')
83
+ };
84
+ const themeSettingsData = customThemeSettingsCache.getAll();
85
+ const productData = await getProductAndPricesData();
86
+ const priceData = calculateLegacyPriceData(productData);
87
+
88
+ let products = null;
89
+ let product = null;
90
+ if (productData.length === 1) {
91
+ product = productData[0];
92
+ } else {
93
+ products = productData;
94
+ }
95
+
96
+ // @TODO: only do this if something changed?
97
+ // @TODO: remove blog in a major where we are happy to break more themes
98
+ {
99
+ hbs.updateTemplateOptions({
100
+ data: {
101
+ blog: siteData,
102
+ site: siteData,
103
+ labs: labsData,
104
+ config: themeData,
105
+ price: priceData,
106
+ product,
107
+ products,
108
+ custom: themeSettingsData
109
+ }
110
+ });
111
+ }
112
+
113
+ next();
114
+ }
115
+
116
+ module.exports = updateGlobalTemplateOptions;
@@ -0,0 +1,9 @@
1
+ function updateLocalTemplateData(req, res, next) {
2
+ // Pass 'secure' flag to the view engine
3
+ // so that templates can choose to render https or http 'url', see url utility
4
+ res.locals.secure = req.secure;
5
+
6
+ next();
7
+ }
8
+
9
+ module.exports = updateLocalTemplateData;
@@ -0,0 +1,57 @@
1
+ const _ = require('lodash');
2
+ const hbs = require('../engine');
3
+ const urlUtils = require('../../../../shared/url-utils');
4
+ const customThemeSettingsCache = require('../../../../shared/custom-theme-settings-cache');
5
+ const labs = require('../../../../shared/labs');
6
+ const preview = require('../preview');
7
+
8
+ function updateLocalTemplateOptions(req, res, next) {
9
+ const localTemplateOptions = hbs.getLocalTemplateOptions(res.locals);
10
+
11
+ // adjust @site.url for http/https based on the incoming request
12
+ const siteData = {
13
+ url: urlUtils.urlFor('home', {secure: req.secure, trailingSlash: false}, true)
14
+ };
15
+
16
+ // @TODO: it would be nicer if this was proper middleware somehow...
17
+ const previewData = preview.handle(req, Object.keys(customThemeSettingsCache.getAll()));
18
+
19
+ // strip custom off of preview data so it doesn't get merged into @site
20
+ const customThemeSettingsPreviewData = previewData.custom;
21
+ delete previewData.custom;
22
+ let customData = {};
23
+ if (labs.isSet('customThemeSettings')) {
24
+ customData = customThemeSettingsPreviewData;
25
+ }
26
+
27
+ // update site data with any preview values from the request
28
+ Object.assign(siteData, previewData);
29
+
30
+ const member = req.member ? {
31
+ uuid: req.member.uuid,
32
+ email: req.member.email,
33
+ name: req.member.name,
34
+ firstname: req.member.name && req.member.name.split(' ')[0],
35
+ avatar_image: req.member.avatar_image,
36
+ subscriptions: req.member.subscriptions && req.member.subscriptions.map((sub) => {
37
+ return Object.assign({}, sub, {
38
+ default_payment_card_last4: sub.default_payment_card_last4 || '****'
39
+ });
40
+ }),
41
+ paid: req.member.status !== 'free'
42
+ } : null;
43
+
44
+ hbs.updateLocalTemplateOptions(res.locals, _.merge({}, localTemplateOptions, {
45
+ data: {
46
+ member: member,
47
+ site: siteData,
48
+ custom: customData,
49
+ // @deprecated: a gscan warning for @blog was added before 3.0 which replaced it with @site
50
+ blog: siteData
51
+ }
52
+ }));
53
+
54
+ next();
55
+ }
56
+
57
+ module.exports = updateLocalTemplateOptions;
@@ -6,6 +6,10 @@
6
6
  justify-content: center;
7
7
  }
8
8
 
9
+ .kg-button-card.kg-align-left {
10
+ justify-content: flex-start;
11
+ }
12
+
9
13
  a.kg-btn {
10
14
  display: flex;
11
15
  position: static;
@@ -1,43 +1,43 @@
1
- .kg-card-callout {
1
+ .kg-callout-card {
2
2
  display: flex;
3
3
  padding: 20px 28px;
4
4
  border-radius: 3px;
5
5
  }
6
6
 
7
- .kg-card-callout-grey {
7
+ .kg-callout-card-grey {
8
8
  background: rgba(124, 139, 154, 0.13);
9
9
  }
10
10
 
11
- .kg-card-callout-white {
11
+ .kg-callout-card-white {
12
12
  background: transparent;
13
13
  box-shadow: inset 0 0 0 1px rgba(124, 139, 154, 0.25);
14
14
  }
15
15
 
16
- .kg-card-callout-blue {
16
+ .kg-callout-card-blue {
17
17
  background: rgba(33, 172, 232, 0.12);
18
18
  }
19
19
 
20
- .kg-card-callout-green {
20
+ .kg-callout-card-green {
21
21
  background: rgba(52, 183, 67, 0.12);
22
22
  }
23
23
 
24
- .kg-card-callout-yellow {
24
+ .kg-callout-card-yellow {
25
25
  background: rgba(240, 165, 15, 0.13);
26
26
  }
27
27
 
28
- .kg-card-callout-red {
28
+ .kg-callout-card-red {
29
29
  background: rgba(209, 46, 46, 0.11);
30
30
  }
31
31
 
32
- .kg-card-callout-pink {
32
+ .kg-callout-card-pink {
33
33
  background: rgba(225, 71, 174, 0.11);
34
34
  }
35
35
 
36
- .kg-card-callout-purple {
36
+ .kg-callout-card-purple {
37
37
  background: rgba(135, 85, 236, 0.12);
38
38
  }
39
39
 
40
- .kg-card-callout-accent {
40
+ .kg-callout-card-accent {
41
41
  background: var(--ghost-accent-color);
42
42
  color: #fff;
43
43
  }
@@ -0,0 +1,93 @@
1
+ const hbs = require('express-hbs');
2
+ const _ = require('lodash');
3
+ const tpl = require('@tryghost/tpl');
4
+ const sentry = require('../../../shared/sentry');
5
+
6
+ const config = require('../../../shared/config');
7
+ const helpers = require('../../services/routing/helpers');
8
+
9
+ // @TODO: make this properly shared code
10
+ const shared = require('../../../server/web/shared/middleware/error-handler');
11
+
12
+ const messages = {
13
+ oopsErrorTemplateHasError: 'Oops, seems there is an error in the error template.',
14
+ encounteredError: 'Encountered the error: ',
15
+ whilstTryingToRender: 'whilst trying to render an error page for the error: '
16
+ };
17
+
18
+ const escapeExpression = hbs.Utils.escapeExpression;
19
+
20
+ /**
21
+ * This is a bare minimum setup, which allows us to render the error page
22
+ * It uses the {{asset}} helper, and nothing more
23
+ */
24
+ const createHbsEngine = () => {
25
+ const engine = hbs.create();
26
+ engine.registerHelper('asset', require('../../helpers/asset'));
27
+
28
+ return engine.express4();
29
+ };
30
+
31
+ const errorFallbackMessage = err => `<h1>${tpl(messages.oopsErrorTemplateHasError)}</h1>
32
+ <p>${tpl(messages.encounteredError)}</p>
33
+ <pre>${escapeExpression(err.message || err)}</pre>
34
+ <br ><p>${tpl(messages.whilstTryingToRender)}</p>
35
+ ${err.statusCode} <pre>${escapeExpression(err.message || err)}</pre>`;
36
+
37
+ const themeErrorRenderer = (err, req, res, next) => {
38
+ // If the error code is explicitly set to STATIC_FILE_NOT_FOUND,
39
+ // Skip trying to render an HTML error, and move on to the basic error renderer
40
+ // We do this because customised 404 templates could reference the image that's missing
41
+ // A better long term solution might be to do this based on extension
42
+ if (err.code === 'STATIC_FILE_NOT_FOUND') {
43
+ return next(err);
44
+ }
45
+
46
+ // Renderer begin
47
+ // Format Data
48
+ const data = {
49
+ message: err.message,
50
+ // @deprecated Remove in Ghost 5.0
51
+ code: err.statusCode,
52
+ statusCode: err.statusCode,
53
+ errorDetails: err.errorDetails || []
54
+ };
55
+
56
+ // Template
57
+ // @TODO: very dirty !!!!!!
58
+ helpers.templates.setTemplate(req, res);
59
+
60
+ // It can be that something went wrong with the theme or otherwise loading handlebars
61
+ // This ensures that no matter what res.render will work here
62
+ // @TODO: split the error handler for assets, admin & theme to refactor this away
63
+ if (_.isEmpty(req.app.engines)) {
64
+ res._template = 'error';
65
+ req.app.engine('hbs', createHbsEngine());
66
+ req.app.set('view engine', 'hbs');
67
+ req.app.set('views', config.get('paths').defaultViews);
68
+ }
69
+
70
+ // @TODO use renderer here?!
71
+ // Render Call - featuring an error handler for what happens if rendering fails
72
+ res.render(res._template, data, (_err, html) => {
73
+ if (!_err) {
74
+ return res.send(html);
75
+ }
76
+
77
+ // re-attach new error e.g. error template has syntax error or misusage
78
+ req.err = _err;
79
+
80
+ // And then try to explain things to the user...
81
+ // Cheat and output the error using handlebars escapeExpression
82
+ return res.status(500).send(errorFallbackMessage(_err));
83
+ });
84
+ };
85
+
86
+ module.exports.handleThemeResponse = [
87
+ // Make sure the error can be served
88
+ shared.prepareError,
89
+ // Handle the error in Sentry
90
+ sentry.errorHandler,
91
+ // Render the error using theme template
92
+ themeErrorRenderer
93
+ ];
@@ -1,6 +1,6 @@
1
1
  const _ = require('lodash');
2
2
  const path = require('path');
3
- const {GhostError} = require('@tryghost/errors');
3
+ const {NoContentError} = require('@tryghost/errors');
4
4
  const imageTransform = require('@tryghost/image-transform');
5
5
  const storage = require('../../../server/adapters/storage');
6
6
  const activeTheme = require('../../services/theme-engine/active');
@@ -102,10 +102,7 @@ module.exports = function (req, res, next) {
102
102
  })
103
103
  .then((originalImageBuffer) => {
104
104
  if (originalImageBuffer.length <= 0) {
105
- throw new GhostError({
106
- errorType: 'NoContentError',
107
- statusCode: 204
108
- });
105
+ throw new NoContentError();
109
106
  }
110
107
  return imageTransform.resizeFromBuffer(originalImageBuffer, imageDimensionConfig);
111
108
  })
@@ -115,7 +112,7 @@ module.exports = function (req, res, next) {
115
112
  }).then(() => {
116
113
  next();
117
114
  }).catch(function (err) {
118
- if (err.code === 'SHARP_INSTALLATION' || err.errorType === 'NoContentError') {
115
+ if (err.code === 'SHARP_INSTALLATION' || err.code === 'IMAGE_PROCESSING' || err.errorType === 'NoContentError') {
119
116
  return redirectToOriginal();
120
117
  }
121
118
  next(err);
@@ -1,4 +1,5 @@
1
1
  module.exports = {
2
+ errorHandler: require('./error-handler'),
2
3
  handleImageSizes: require('./handle-image-sizes'),
3
4
  redirectGhostToAdmin: require('./redirect-ghost-to-admin'),
4
5
  serveFavicon: require('./serve-favicon'),
@@ -184,7 +184,7 @@ module.exports = function setupSiteApp(options = {}) {
184
184
  app.setupErrorHandling(siteApp);
185
185
  }
186
186
  });
187
- siteApp.use(shared.middleware.errorHandler.handleThemeResponse);
187
+ siteApp.use(mw.errorHandler.handleThemeResponse);
188
188
 
189
189
  debug('Site setup end');
190
190
 
@@ -307,7 +307,7 @@ SchedulingDefault.prototype._pingUrl = function (object) {
307
307
  this._pingUrl(object);
308
308
  }, this.retryTimeoutInMs);
309
309
 
310
- logging.error(new errors.GhostError({
310
+ logging.error(new errors.InternalServerError({
311
311
  err,
312
312
  context: 'Retrying...',
313
313
  level: 'normal'
@@ -316,7 +316,7 @@ SchedulingDefault.prototype._pingUrl = function (object) {
316
316
  return;
317
317
  }
318
318
 
319
- logging.error(new errors.GhostError({
319
+ logging.error(new errors.InternalServerError({
320
320
  err,
321
321
  level: 'critical'
322
322
  }));
@@ -147,7 +147,7 @@ class LocalStorageBase extends StorageBase {
147
147
  return next(new errors.NoPermissionError({err: err}));
148
148
  }
149
149
 
150
- return next(new errors.GhostError({err: err}));
150
+ return next(new errors.InternalServerError({err: err}));
151
151
  }
152
152
 
153
153
  next();
@@ -196,7 +196,7 @@ class LocalStorageBase extends StorageBase {
196
196
  return reject(new errors.NoPermissionError({err: err}));
197
197
  }
198
198
 
199
- return reject(new errors.GhostError({
199
+ return reject(new errors.InternalServerError({
200
200
  err: err,
201
201
  message: tpl(this.errorMessages.cannotRead, {file: options.path})
202
202
  }));
@@ -62,7 +62,7 @@ module.exports = {
62
62
  return Promise.resolve()
63
63
  .then(() => exporter.doExport({include: frame.options.withRelated}))
64
64
  .catch((err) => {
65
- return Promise.reject(new errors.GhostError({err: err}));
65
+ return Promise.reject(new errors.InternalServerError({err: err}));
66
66
  });
67
67
  }
68
68
  },
@@ -124,7 +124,7 @@ module.exports = {
124
124
  }, {concurrency: 100});
125
125
  })
126
126
  .catch((err) => {
127
- throw new errors.GhostError({
127
+ throw new errors.InternalServerError({
128
128
  err: err
129
129
  });
130
130
  });
@@ -23,8 +23,9 @@ module.exports = {
23
23
 
24
24
  uploadThumbnail: {
25
25
  permissions: false,
26
- options: [
27
- 'url'
26
+ data: [
27
+ 'url',
28
+ 'ref'
28
29
  ],
29
30
  async query(frame) {
30
31
  const mediaStorage = storage.getStorage('media');
@@ -3,9 +3,24 @@ const externalRequest = require('../../lib/request-external');
3
3
 
4
4
  const OEmbed = require('../../services/oembed');
5
5
  const oembed = new OEmbed({config, externalRequest});
6
+
6
7
  const NFT = require('../../services/nft-oembed');
7
- const nft = new NFT();
8
+ const nft = new NFT({
9
+ config: {
10
+ apiKey: config.get('opensea').privateReadOnlyApiKey
11
+ }
12
+ });
13
+
14
+ const Twitter = require('../../services/twitter-embed');
15
+ const twitter = new Twitter({
16
+ config: {
17
+ bearerToken: config.get('twitter').privateReadOnlyToken
18
+ },
19
+ logging: require('@tryghost/logging')
20
+ });
21
+
8
22
  oembed.registerProvider(nft);
23
+ oembed.registerProvider(twitter);
9
24
 
10
25
  module.exports = {
11
26
  docName: 'oembed',
@@ -42,7 +42,7 @@ const session = {
42
42
  });
43
43
  });
44
44
  }).catch(async (err) => {
45
- if (!errors.utils.isIgnitionError(err)) {
45
+ if (!errors.utils.isGhostError(err)) {
46
46
  throw new errors.UnauthorizedError({
47
47
  message: tpl(messages.accessDenied),
48
48
  err
@@ -43,7 +43,7 @@ module.exports = {
43
43
  return models.Base.Model.generateSlug(allowedTypes[frame.options.type], frame.data.name, {status: 'all'})
44
44
  .then((slug) => {
45
45
  if (!slug) {
46
- return Promise.reject(new errors.GhostError({
46
+ return Promise.reject(new errors.InternalServerError({
47
47
  message: tpl(messages.couldNotGenerateSlug)
48
48
  }));
49
49
  }
@@ -66,11 +66,11 @@ const nonePublicAuth = (apiConfig, frame) => {
66
66
  return Promise.reject(err);
67
67
  }
68
68
 
69
- if (errors.utils.isIgnitionError(err)) {
69
+ if (errors.utils.isGhostError(err)) {
70
70
  return Promise.reject(err);
71
71
  }
72
72
 
73
- return Promise.reject(new errors.GhostError({
73
+ return Promise.reject(new errors.InternalServerError({
74
74
  err: err
75
75
  }));
76
76
  });
@@ -1,5 +1,4 @@
1
1
  const _ = require('lodash');
2
- const labs = require('../../../../../../shared/labs');
3
2
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:config');
4
3
 
5
4
  module.exports = {
@@ -18,13 +17,10 @@ module.exports = {
18
17
  'stripeDirect',
19
18
  'mailgunIsConfigured',
20
19
  'emailAnalytics',
21
- 'hostSettings'
20
+ 'hostSettings',
21
+ 'tenor'
22
22
  ];
23
23
 
24
- if (labs.isSet('gifsCard')) {
25
- keys.push('tenor');
26
- }
27
-
28
24
  frame.response = {
29
25
  config: _.pick(data, keys)
30
26
  };