ghost 4.20.4 → 4.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +7 -1
- package/Gruntfile.js +2 -0
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +263 -50
- package/content/themes/casper/default.hbs +12 -3
- package/content/themes/casper/index.hbs +25 -23
- package/content/themes/casper/package.json +91 -2
- package/content/themes/casper/partials/post-card.hbs +1 -1
- package/content/themes/casper/post.hbs +18 -14
- package/content/themes/casper/yarn.lock +245 -192
- package/core/boot.js +8 -0
- package/core/bridge.js +14 -0
- package/core/built/assets/{chunk.3.777d43e2ce954ba8b2f5.js → chunk.3.324fd0cc598c73650219.js} +59 -59
- package/core/built/assets/{ghost-dark-20e2892d4f30d0d1183c9ac725ea37d0.css → ghost-dark-39fb496d051565531062d7e047d1c0b1.css} +1 -1
- package/core/built/assets/{ghost.min-57e46fd3b1145ecf2cbd185a13611f3b.css → ghost.min-4207edfc1ae0a3f9f6505ca00d20b0c0.css} +1 -1
- package/core/built/assets/{ghost.min-07b6a50c54b3e2e190332c28c7255d2f.js → ghost.min-7da921f6c6cac3fe10da1ba104575440.js} +1802 -1911
- package/core/built/assets/{vendor.min-af502ac4142871500fc424f6a5a254ec.js → vendor.min-413f887176a041e6dbf88214ca9a7481.js} +7039 -6879
- package/core/frontend/apps/amp/lib/helpers/amp_content.js +16 -4
- package/core/frontend/helpers/asset.js +9 -1
- package/core/frontend/helpers/ghost_head.js +13 -1
- package/core/frontend/meta/title.js +15 -5
- package/core/frontend/services/card-assets/index.js +16 -0
- package/core/frontend/services/card-assets/service.js +101 -0
- package/core/frontend/services/theme-engine/config/defaults.json +4 -1
- package/core/frontend/services/theme-engine/config/index.js +1 -1
- package/core/frontend/src/cards/css/bookmark.css +83 -0
- package/core/frontend/src/cards/css/button.css +30 -0
- package/core/frontend/src/cards/css/callout.css +12 -0
- package/core/frontend/src/cards/css/gallery.css +36 -0
- package/core/frontend/src/cards/css/nft.css +85 -0
- package/core/frontend/src/cards/js/gallery.js +8 -0
- package/core/frontend/web/middleware/serve-public-file.js +10 -1
- package/core/frontend/web/routes.js +0 -1
- package/core/frontend/web/site.js +13 -9
- package/core/server/adapters/storage/LocalFilesStorage.js +17 -0
- package/core/server/adapters/storage/LocalImagesStorage.js +51 -0
- package/core/server/adapters/storage/LocalMediaStorage.js +24 -0
- package/core/server/adapters/storage/{LocalFileStorage.js → LocalStorageBase.js} +64 -51
- package/core/server/adapters/storage/index.js +1 -1
- package/core/server/adapters/storage/utils.js +2 -2
- package/core/server/api/canary/files.js +19 -0
- package/core/server/api/canary/index.js +8 -0
- package/core/server/api/canary/media.js +42 -0
- package/core/server/api/canary/members.js +6 -103
- package/core/server/api/canary/membersStripeConnect.js +0 -10
- package/core/server/api/canary/oembed.js +3 -0
- package/core/server/api/canary/redirects.js +1 -6
- package/core/server/api/canary/utils/serializers/input/index.js +4 -0
- package/core/server/api/canary/utils/serializers/input/media.js +8 -0
- package/core/server/api/canary/utils/serializers/input/pages.js +8 -0
- package/core/server/api/canary/utils/serializers/output/config.js +21 -14
- package/core/server/api/canary/utils/serializers/output/files.js +27 -0
- package/core/server/api/canary/utils/serializers/output/images.js +4 -0
- package/core/server/api/canary/utils/serializers/output/index.js +8 -0
- package/core/server/api/canary/utils/serializers/output/media.js +37 -0
- package/core/server/api/canary/utils/validators/input/files.js +7 -0
- package/core/server/api/canary/utils/validators/input/index.js +8 -0
- package/core/server/api/canary/utils/validators/input/media.js +11 -0
- package/core/server/api/v2/redirects.js +1 -6
- package/core/server/api/v2/utils/serializers/output/images.js +4 -0
- package/core/server/api/v3/members.js +5 -1
- package/core/server/api/v3/redirects.js +1 -6
- package/core/server/api/v3/utils/serializers/output/images.js +4 -0
- package/core/server/data/migrations/utils.js +55 -16
- package/core/server/data/migrations/versions/4.22/01-add-is-launch-complete-setting.js +8 -0
- package/core/server/data/migrations/versions/4.22/02-update-launch-complete-setting-from-user-data.js +39 -0
- package/core/server/data/schema/default-settings.json +8 -0
- package/core/server/frontend/ghost.min.css +1 -1
- package/core/server/lib/image/blog-icon.js +2 -4
- package/core/server/lib/image/image-size.js +1 -1
- package/core/server/services/limits.js +3 -6
- package/core/server/services/mega/template.js +62 -1
- package/core/server/services/members/api.js +1 -1
- package/core/server/services/members/emails/signup.js +2 -2
- package/core/server/services/members/stripe-connect.js +14 -0
- package/core/server/services/nft-oembed.js +71 -0
- package/core/server/services/oembed.js +145 -110
- package/core/server/services/offers/service.js +1 -31
- package/core/server/services/public-config/config.js +2 -1
- package/core/server/services/redirects/api.js +270 -0
- package/core/server/services/redirects/index.js +27 -12
- package/core/server/services/stripe/index.js +4 -2
- package/core/server/services/themes/ThemeStorage.js +5 -5
- package/core/server/services/url/Resource.js +1 -1
- package/core/server/services/url/Resources.js +28 -21
- package/core/server/services/url/UrlService.js +66 -8
- package/core/server/services/url/Urls.js +7 -2
- package/core/server/services/url/index.js +8 -1
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/canary/admin/routes.js +28 -4
- package/core/server/web/api/middleware/cors.js +7 -7
- package/core/server/web/api/middleware/upload.js +117 -10
- package/core/server/web/members/app.js +1 -1
- package/core/server/web/shared/middlewares/index.js +0 -4
- package/core/shared/config/defaults.json +5 -1
- package/core/shared/config/helpers.js +4 -0
- package/core/shared/config/overrides.json +8 -0
- package/core/shared/labs.js +12 -3
- package/package.json +47 -46
- package/urls.json +597 -0
- package/yarn.lock +1073 -1075
- package/core/built/assets/img/themes/Editorial-a25a4a34c04dedd858bd5e05ef388b1c.jpg +0 -0
- package/core/built/assets/img/themes/Massively-06edf00108429f7fb8e65f190fba34fe.jpg +0 -0
- package/core/server/services/redirects/settings.js +0 -234
- package/core/server/web/shared/middlewares/custom-redirects.js +0 -128
|
@@ -4,6 +4,9 @@ const {Buffer} = require('buffer');
|
|
|
4
4
|
const {randomBytes} = require('crypto');
|
|
5
5
|
const {URL} = require('url');
|
|
6
6
|
|
|
7
|
+
const config = require('../../../shared/config');
|
|
8
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
9
|
+
|
|
7
10
|
const messages = {
|
|
8
11
|
incorrectState: 'State did not match.'
|
|
9
12
|
};
|
|
@@ -24,6 +27,7 @@ const redirectURI = 'https://stripe.ghost.org';
|
|
|
24
27
|
* @returns {Promise<URL>}
|
|
25
28
|
*/
|
|
26
29
|
async function getStripeConnectOAuthUrl(setSessionProp, mode = 'live') {
|
|
30
|
+
checkCanConnect();
|
|
27
31
|
const randomState = randomBytes(16).toString('hex');
|
|
28
32
|
const state = Buffer.from(JSON.stringify({
|
|
29
33
|
mode,
|
|
@@ -71,6 +75,16 @@ async function getStripeConnectTokenData(encodedData, getSessionProp) {
|
|
|
71
75
|
};
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
function checkCanConnect() {
|
|
79
|
+
const siteUrl = urlUtils.getSiteUrl();
|
|
80
|
+
const productionMode = config.get('env') === 'production';
|
|
81
|
+
const siteUrlUsingSSL = /^https/.test(siteUrl);
|
|
82
|
+
const cannotConnectToStripe = productionMode && !siteUrlUsingSSL;
|
|
83
|
+
if (cannotConnectToStripe) {
|
|
84
|
+
throw new errors.BadRequestError('Cannot connect to stripe unless site is using https://');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
74
88
|
module.exports = {
|
|
75
89
|
getStripeConnectOAuthUrl,
|
|
76
90
|
getStripeConnectTokenData,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./oembed').ICustomProvider} ICustomProvider
|
|
3
|
+
* @typedef {import('./oembed').IExternalRequest} IExternalRequest
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const OPENSEA_PATH_REGEX = /^\/assets\/(0x[a-f0-9]+)\/(\d+)/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @implements ICustomProvider
|
|
10
|
+
*/
|
|
11
|
+
class NFTOEmbedProvider {
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} dependencies
|
|
14
|
+
*/
|
|
15
|
+
constructor(dependencies) {
|
|
16
|
+
this.dependencies = dependencies;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {URL} url
|
|
21
|
+
* @returns {Promise<boolean>}
|
|
22
|
+
*/
|
|
23
|
+
async canSupportRequest(url) {
|
|
24
|
+
return url.host === 'opensea.io' && OPENSEA_PATH_REGEX.test(url.pathname);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {URL} url
|
|
29
|
+
* @param {IExternalRequest} externalRequest
|
|
30
|
+
*
|
|
31
|
+
* @returns {Promise<import('oembed-parser').RichTypeData & Object<string, any>>}
|
|
32
|
+
*/
|
|
33
|
+
async getOEmbedData(url, externalRequest) {
|
|
34
|
+
const [match, transaction, asset] = url.pathname.match(OPENSEA_PATH_REGEX);
|
|
35
|
+
if (!match) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const result = await externalRequest(`https://api.opensea.io/api/v1/asset/${transaction}/${asset}/`, {
|
|
39
|
+
json: true
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
version: '1.0',
|
|
43
|
+
type: 'rich',
|
|
44
|
+
title: result.body.name,
|
|
45
|
+
author_name: result.body.creator.user.username,
|
|
46
|
+
author_url: `https://opensea.io/${result.body.creator.user.username}`,
|
|
47
|
+
provider_name: 'OpenSea',
|
|
48
|
+
provider_url: 'https://opensea.io',
|
|
49
|
+
html: `
|
|
50
|
+
<a href="${result.body.permalink}" class="kg-nft-card">
|
|
51
|
+
<img class="kg-nft-image" src="${result.body.image_url}">
|
|
52
|
+
<div class="kg-nft-metadata">
|
|
53
|
+
<div class="kg-nft-header">
|
|
54
|
+
<h4 class="kg-nft-title"> ${result.body.name} </h4>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="kg-nft-creator">
|
|
57
|
+
Created by <span class="kg-nft-creator-name">${result.body.creator.user.username}</span>
|
|
58
|
+
${(result.body.collection.name ? `• ${result.body.collection.name}` : ``)}
|
|
59
|
+
</div>
|
|
60
|
+
${(result.body.description ? `<p class="kg-nft-description">${result.body.description}</p>` : ``)}
|
|
61
|
+
</div>
|
|
62
|
+
</a>
|
|
63
|
+
`,
|
|
64
|
+
width: 1000,
|
|
65
|
+
height: 1000,
|
|
66
|
+
noIframe: true
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = NFTOEmbedProvider;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const Promise = require('bluebird');
|
|
2
1
|
const errors = require('@tryghost/errors');
|
|
3
2
|
const tpl = require('@tryghost/tpl');
|
|
4
3
|
const {extract, hasProvider} = require('oembed-parser');
|
|
@@ -11,6 +10,10 @@ const messages = {
|
|
|
11
10
|
insufficientMetadata: 'URL contains insufficient metadata.'
|
|
12
11
|
};
|
|
13
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} url
|
|
15
|
+
* @returns {{url: string, provider: boolean}}
|
|
16
|
+
*/
|
|
14
17
|
const findUrlWithProvider = (url) => {
|
|
15
18
|
let provider;
|
|
16
19
|
|
|
@@ -44,6 +47,12 @@ const findUrlWithProvider = (url) => {
|
|
|
44
47
|
* @typedef {(url: string, config: Object) => Promise} IExternalRequest
|
|
45
48
|
*/
|
|
46
49
|
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {object} ICustomProvider
|
|
52
|
+
* @prop {(url: URL) => Promise<boolean>} canSupportRequest
|
|
53
|
+
* @prop {(url: URL, externalRequest: IExternalRequest) => Promise<import('oembed-parser').OembedData>} getOEmbedData
|
|
54
|
+
*/
|
|
55
|
+
|
|
47
56
|
class OEmbed {
|
|
48
57
|
/**
|
|
49
58
|
*
|
|
@@ -53,29 +62,62 @@ class OEmbed {
|
|
|
53
62
|
*/
|
|
54
63
|
constructor({config, externalRequest}) {
|
|
55
64
|
this.config = config;
|
|
56
|
-
|
|
65
|
+
/** @type {IExternalRequest} */
|
|
66
|
+
this.externalRequest = async (url, requestConfig) => {
|
|
67
|
+
if (this.isIpOrLocalhost(url)) {
|
|
68
|
+
return this.unknownProvider(url);
|
|
69
|
+
}
|
|
70
|
+
const response = await externalRequest(url, requestConfig);
|
|
71
|
+
if (this.isIpOrLocalhost(response.url)) {
|
|
72
|
+
return this.unknownProvider(url);
|
|
73
|
+
}
|
|
74
|
+
return response;
|
|
75
|
+
};
|
|
76
|
+
/** @type {ICustomProvider[]} */
|
|
77
|
+
this.customProviders = [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {ICustomProvider} provider
|
|
82
|
+
*/
|
|
83
|
+
registerProvider(provider) {
|
|
84
|
+
this.customProviders.push(provider);
|
|
57
85
|
}
|
|
58
86
|
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} url
|
|
89
|
+
*/
|
|
90
|
+
async unknownProvider(url) {
|
|
91
|
+
throw new errors.ValidationError({
|
|
61
92
|
message: tpl(messages.unknownProvider),
|
|
62
93
|
context: url
|
|
63
|
-
})
|
|
94
|
+
});
|
|
64
95
|
}
|
|
65
96
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} url
|
|
99
|
+
*/
|
|
100
|
+
async knownProvider(url) {
|
|
101
|
+
try {
|
|
102
|
+
return await extract(url);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
throw new errors.InternalServerError({
|
|
69
105
|
message: err.message
|
|
70
|
-
})
|
|
71
|
-
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
72
108
|
}
|
|
73
109
|
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} url
|
|
112
|
+
*/
|
|
74
113
|
errorHandler(url) {
|
|
75
|
-
|
|
114
|
+
/**
|
|
115
|
+
* @param {Error|errors.GhostError} err
|
|
116
|
+
*/
|
|
117
|
+
return async (err) => {
|
|
76
118
|
// allow specific validation errors through for better error messages
|
|
77
119
|
if (errors.utils.isIgnitionError(err) && err.errorType === 'ValidationError') {
|
|
78
|
-
|
|
120
|
+
throw err;
|
|
79
121
|
}
|
|
80
122
|
|
|
81
123
|
// default to unknown provider to avoid leaking any app specifics
|
|
@@ -97,19 +139,11 @@ class OEmbed {
|
|
|
97
139
|
|
|
98
140
|
let scraperResponse;
|
|
99
141
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const response = await this.externalRequest(url, {cookieJar});
|
|
142
|
+
const cookieJar = new CookieJar();
|
|
143
|
+
const response = await this.externalRequest(url, {cookieJar});
|
|
103
144
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} else {
|
|
107
|
-
const html = response.body;
|
|
108
|
-
scraperResponse = await metascraper({html, url});
|
|
109
|
-
}
|
|
110
|
-
} catch (err) {
|
|
111
|
-
return Promise.reject(err);
|
|
112
|
-
}
|
|
145
|
+
const html = response.body;
|
|
146
|
+
scraperResponse = await metascraper({html, url});
|
|
113
147
|
|
|
114
148
|
const metadata = Object.assign({}, scraperResponse, {
|
|
115
149
|
thumbnail: scraperResponse.image,
|
|
@@ -119,20 +153,25 @@ class OEmbed {
|
|
|
119
153
|
delete metadata.image;
|
|
120
154
|
delete metadata.logo;
|
|
121
155
|
|
|
122
|
-
if (metadata.title) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
url
|
|
126
|
-
metadata
|
|
156
|
+
if (!metadata.title) {
|
|
157
|
+
throw new errors.ValidationError({
|
|
158
|
+
message: tpl(messages.insufficientMetadata),
|
|
159
|
+
context: url
|
|
127
160
|
});
|
|
128
161
|
}
|
|
129
162
|
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
163
|
+
return {
|
|
164
|
+
version: '1.0',
|
|
165
|
+
type: 'bookmark',
|
|
166
|
+
url,
|
|
167
|
+
metadata
|
|
168
|
+
};
|
|
134
169
|
}
|
|
135
170
|
|
|
171
|
+
/**
|
|
172
|
+
* @param {string} url
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
136
175
|
isIpOrLocalhost(url) {
|
|
137
176
|
try {
|
|
138
177
|
const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
|
@@ -163,14 +202,7 @@ class OEmbed {
|
|
|
163
202
|
*
|
|
164
203
|
* @returns {Promise<Object>}
|
|
165
204
|
*/
|
|
166
|
-
fetchOembedData(_url, cardType) {
|
|
167
|
-
// parse the url then validate the protocol and host to make sure it's
|
|
168
|
-
// http(s) and not an IP address or localhost to avoid potential access to
|
|
169
|
-
// internal network endpoints
|
|
170
|
-
if (this.isIpOrLocalhost(_url)) {
|
|
171
|
-
return this.unknownProvider();
|
|
172
|
-
}
|
|
173
|
-
|
|
205
|
+
async fetchOembedData(_url, cardType) {
|
|
174
206
|
// check against known oembed list
|
|
175
207
|
let {url, provider} = findUrlWithProvider(_url);
|
|
176
208
|
if (provider) {
|
|
@@ -180,88 +212,81 @@ class OEmbed {
|
|
|
180
212
|
// url not in oembed list so fetch it in case it's a redirect or has a
|
|
181
213
|
// <link rel="alternate" type="application/json+oembed"> element
|
|
182
214
|
const cookieJar = new CookieJar();
|
|
183
|
-
|
|
215
|
+
const pageResponse = await this.externalRequest(url, {
|
|
184
216
|
method: 'GET',
|
|
185
217
|
timeout: 2 * 1000,
|
|
186
218
|
followRedirect: true,
|
|
187
219
|
cookieJar
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
220
|
+
});
|
|
221
|
+
// url changed after fetch, see if we were redirected to a known oembed
|
|
222
|
+
if (pageResponse.url !== url) {
|
|
223
|
+
({url, provider} = findUrlWithProvider(pageResponse.url));
|
|
224
|
+
if (provider) {
|
|
225
|
+
return this.knownProvider(url);
|
|
195
226
|
}
|
|
227
|
+
}
|
|
196
228
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
229
|
+
// check for <link rel="alternate" type="application/json+oembed"> element
|
|
230
|
+
let oembedUrl;
|
|
231
|
+
try {
|
|
232
|
+
oembedUrl = cheerio('link[type="application/json+oembed"]', pageResponse.body).attr('href');
|
|
233
|
+
} catch (e) {
|
|
234
|
+
return this.unknownProvider(url);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (oembedUrl) {
|
|
238
|
+
// for standard WP oembed's we want to insert a bookmark card rather than their blockquote+script
|
|
239
|
+
// which breaks in the editor and most Ghost themes. Only fallback if card type was not explicitly chosen
|
|
240
|
+
if (!cardType && oembedUrl.match(/wp-json\/oembed/)) {
|
|
241
|
+
return;
|
|
203
242
|
}
|
|
204
243
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
244
|
+
// fetch oembed response from embedded rel="alternate" url
|
|
245
|
+
const oembedResponse = await this.externalRequest(oembedUrl, {
|
|
246
|
+
method: 'GET',
|
|
247
|
+
json: true,
|
|
248
|
+
timeout: 2 * 1000,
|
|
249
|
+
followRedirect: true,
|
|
250
|
+
cookieJar
|
|
251
|
+
});
|
|
252
|
+
// validate the fetched json against the oembed spec to avoid
|
|
253
|
+
// leaking non-oembed responses
|
|
254
|
+
const body = oembedResponse.body;
|
|
255
|
+
const hasRequiredFields = body.type && body.version;
|
|
256
|
+
const hasValidType = ['photo', 'video', 'link', 'rich'].includes(body.type);
|
|
257
|
+
|
|
258
|
+
if (hasRequiredFields && hasValidType) {
|
|
259
|
+
// extract known oembed fields from the response to limit leaking of unrecognised data
|
|
260
|
+
const knownFields = [
|
|
261
|
+
'type',
|
|
262
|
+
'version',
|
|
263
|
+
'html',
|
|
264
|
+
'url',
|
|
265
|
+
'title',
|
|
266
|
+
'width',
|
|
267
|
+
'height',
|
|
268
|
+
'author_name',
|
|
269
|
+
'author_url',
|
|
270
|
+
'provider_name',
|
|
271
|
+
'provider_url',
|
|
272
|
+
'thumbnail_url',
|
|
273
|
+
'thumbnail_width',
|
|
274
|
+
'thumbnail_height'
|
|
275
|
+
];
|
|
276
|
+
const oembed = _.pick(body, knownFields);
|
|
277
|
+
|
|
278
|
+
// ensure we have required data for certain types
|
|
279
|
+
if (oembed.type === 'photo' && !oembed.url) {
|
|
280
|
+
return;
|
|
209
281
|
}
|
|
210
|
-
|
|
211
|
-
// for standard WP oembed's we want to insert a bookmark card rather than their blockquote+script
|
|
212
|
-
// which breaks in the editor and most Ghost themes. Only fallback if card type was not explicitly chosen
|
|
213
|
-
if (!cardType && oembedUrl.match(/wp-json\/oembed/)) {
|
|
282
|
+
if ((oembed.type === 'video' || oembed.type === 'rich') && (!oembed.html || !oembed.width || !oembed.height)) {
|
|
214
283
|
return;
|
|
215
284
|
}
|
|
216
285
|
|
|
217
|
-
//
|
|
218
|
-
return
|
|
219
|
-
method: 'GET',
|
|
220
|
-
json: true,
|
|
221
|
-
timeout: 2 * 1000,
|
|
222
|
-
followRedirect: true,
|
|
223
|
-
cookieJar
|
|
224
|
-
}).then((oembedResponse) => {
|
|
225
|
-
// validate the fetched json against the oembed spec to avoid
|
|
226
|
-
// leaking non-oembed responses
|
|
227
|
-
const body = oembedResponse.body;
|
|
228
|
-
const hasRequiredFields = body.type && body.version;
|
|
229
|
-
const hasValidType = ['photo', 'video', 'link', 'rich'].includes(body.type);
|
|
230
|
-
|
|
231
|
-
if (hasRequiredFields && hasValidType) {
|
|
232
|
-
// extract known oembed fields from the response to limit leaking of unrecognised data
|
|
233
|
-
const knownFields = [
|
|
234
|
-
'type',
|
|
235
|
-
'version',
|
|
236
|
-
'html',
|
|
237
|
-
'url',
|
|
238
|
-
'title',
|
|
239
|
-
'width',
|
|
240
|
-
'height',
|
|
241
|
-
'author_name',
|
|
242
|
-
'author_url',
|
|
243
|
-
'provider_name',
|
|
244
|
-
'provider_url',
|
|
245
|
-
'thumbnail_url',
|
|
246
|
-
'thumbnail_width',
|
|
247
|
-
'thumbnail_height'
|
|
248
|
-
];
|
|
249
|
-
const oembed = _.pick(body, knownFields);
|
|
250
|
-
|
|
251
|
-
// ensure we have required data for certain types
|
|
252
|
-
if (oembed.type === 'photo' && !oembed.url) {
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if ((oembed.type === 'video' || oembed.type === 'rich') && (!oembed.html || !oembed.width || !oembed.height)) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// return the extracted object, don't pass through the response body
|
|
260
|
-
return oembed;
|
|
261
|
-
}
|
|
262
|
-
}).catch(() => {});
|
|
286
|
+
// return the extracted object, don't pass through the response body
|
|
287
|
+
return oembed;
|
|
263
288
|
}
|
|
264
|
-
}
|
|
289
|
+
}
|
|
265
290
|
}
|
|
266
291
|
|
|
267
292
|
/**
|
|
@@ -274,6 +299,16 @@ class OEmbed {
|
|
|
274
299
|
let data;
|
|
275
300
|
|
|
276
301
|
try {
|
|
302
|
+
const urlObject = new URL(url);
|
|
303
|
+
for (const provider of this.customProviders) {
|
|
304
|
+
if (await provider.canSupportRequest(urlObject)) {
|
|
305
|
+
const result = await provider.getOEmbedData(urlObject, this.externalRequest);
|
|
306
|
+
if (result !== null) {
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
277
312
|
if (type === 'bookmark') {
|
|
278
313
|
return this.fetchBookmarkData(url);
|
|
279
314
|
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
const labs = require('../../../shared/labs');
|
|
2
|
-
const events = require('../../lib/common/events');
|
|
3
|
-
|
|
4
1
|
const DynamicRedirectManager = require('@tryghost/express-dynamic-redirects');
|
|
5
2
|
const OffersModule = require('@tryghost/members-offers');
|
|
6
3
|
|
|
@@ -28,34 +25,7 @@ module.exports = {
|
|
|
28
25
|
|
|
29
26
|
this.api = offersModule.api;
|
|
30
27
|
|
|
31
|
-
|
|
32
|
-
if (labs.isSet('offers')) {
|
|
33
|
-
// handles setting up redirects
|
|
34
|
-
const promise = offersModule.init();
|
|
35
|
-
initCalled = true;
|
|
36
|
-
await promise;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// TODO: Delete after GA
|
|
40
|
-
let offersEnabled = labs.isSet('offers');
|
|
41
|
-
events.on('settings.labs.edited', async () => {
|
|
42
|
-
if (labs.isSet('offers') && !initCalled) {
|
|
43
|
-
const promise = offersModule.init();
|
|
44
|
-
initCalled = true;
|
|
45
|
-
await promise;
|
|
46
|
-
} else if (labs.isSet('offers') !== offersEnabled) {
|
|
47
|
-
offersEnabled = labs.isSet('offers');
|
|
48
|
-
|
|
49
|
-
if (offersEnabled) {
|
|
50
|
-
const offers = await this.api.listOffers({});
|
|
51
|
-
for (const offer of offers) {
|
|
52
|
-
redirectManager.addRedirect(`/${offer.code}`, `/#/portal/offers/${offer.id}`, {permanent: false});
|
|
53
|
-
}
|
|
54
|
-
} else {
|
|
55
|
-
redirectManager.removeAllRedirects();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
});
|
|
28
|
+
await offersModule.init();
|
|
59
29
|
},
|
|
60
30
|
|
|
61
31
|
api: null,
|
|
@@ -16,7 +16,8 @@ module.exports = function getConfigProperties() {
|
|
|
16
16
|
stripeDirect: config.get('stripeDirect'),
|
|
17
17
|
mailgunIsConfigured: config.get('bulkEmail') && config.get('bulkEmail').mailgun,
|
|
18
18
|
emailAnalytics: config.get('emailAnalytics'),
|
|
19
|
-
hostSettings: config.get('hostSettings')
|
|
19
|
+
hostSettings: config.get('hostSettings'),
|
|
20
|
+
tenorApiKey: config.get('tenorApiKey')
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
const billingUrl = config.get('hostSettings:billing:enabled') ? config.get('hostSettings:billing:url') : '';
|