ghost 4.22.2 → 4.24.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 (118) hide show
  1. package/.c8rc.json +24 -0
  2. package/Gruntfile.js +0 -1
  3. package/content/public/README.md +3 -0
  4. package/core/app.js +12 -1
  5. package/core/boot.js +45 -26
  6. package/core/bridge.js +10 -10
  7. package/core/built/assets/{chunk.3.324fd0cc598c73650219.js → chunk.3.8f95b516d88ff4eec64c.js} +18 -18
  8. package/core/built/assets/{ghost-dark-39fb496d051565531062d7e047d1c0b1.css → ghost-dark-e7b57ab951512c5719aee89b16b9a448.css} +1 -1
  9. package/core/built/assets/{ghost.min-4207edfc1ae0a3f9f6505ca00d20b0c0.css → ghost.min-7f3603dbeb5ebf0ec09e207ae82fb4e3.css} +1 -1
  10. package/core/built/assets/{ghost.min-7da921f6c6cac3fe10da1ba104575440.js → ghost.min-d5595f9c71ebc534ccf9ac78483d357c.js} +138 -105
  11. package/core/built/assets/icons/powered-by-tenor.svg +35 -0
  12. package/core/built/assets/icons/tenor.svg +7 -0
  13. package/core/built/assets/{vendor.min-413f887176a041e6dbf88214ca9a7481.js → vendor.min-1a84ac3ef74edf31c6e86810b45221cc.js} +2964 -2434
  14. package/core/frontend/apps/amp/lib/views/amp.hbs +104 -0
  15. package/core/frontend/apps/private-blogging/lib/router.js +1 -1
  16. package/core/frontend/services/card-assets/index.js +0 -12
  17. package/core/frontend/services/card-assets/service.js +35 -26
  18. package/core/frontend/services/routing/CollectionRouter.js +4 -5
  19. package/core/frontend/services/routing/EmailRouter.js +1 -1
  20. package/core/frontend/services/routing/ParentRouter.js +0 -8
  21. package/core/frontend/services/routing/PreviewRouter.js +1 -1
  22. package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
  23. package/core/frontend/services/routing/StaticRoutesRouter.js +4 -4
  24. package/core/frontend/services/routing/TaxonomyRouter.js +3 -3
  25. package/core/frontend/services/routing/{middlewares → middleware}/index.js +0 -0
  26. package/core/frontend/services/routing/{middlewares → middleware}/page-param.js +0 -0
  27. package/core/frontend/services/routing/router-manager.js +7 -2
  28. package/core/frontend/services/rss/generate-feed.js +2 -1
  29. package/core/frontend/src/cards/css/bookmark.css +72 -47
  30. package/core/frontend/src/cards/css/callout.css +41 -4
  31. package/core/frontend/src/cards/css/gallery.css +15 -10
  32. package/core/frontend/src/cards/css/nft.css +20 -11
  33. package/core/frontend/src/cards/css/toggle.css +58 -0
  34. package/core/frontend/src/cards/js/toggle.js +16 -0
  35. package/core/frontend/web/middleware/serve-public-file.js +39 -16
  36. package/core/frontend/web/site.js +11 -14
  37. package/core/server/api/canary/authentication.js +1 -1
  38. package/core/server/api/canary/utils/serializers/output/config.js +1 -1
  39. package/core/server/api/v2/authentication.js +1 -1
  40. package/core/server/api/v3/authentication.js +1 -1
  41. package/core/server/data/db/connection.js +7 -0
  42. package/core/server/data/importer/importers/data/data-importer.js +3 -3
  43. package/core/server/data/migrations/init/2-create-fixtures.js +3 -20
  44. package/core/server/data/migrations/versions/1.21/1-add-contributor-role.js +5 -5
  45. package/core/server/data/migrations/versions/2.15/2-insert-zapier-integration.js +3 -3
  46. package/core/server/data/migrations/versions/2.2/3-insert-admin-integration-role.js +5 -5
  47. package/core/server/data/migrations/versions/2.27/1-insert-ghost-db-backup-role.js +5 -6
  48. package/core/server/data/migrations/versions/2.27/2-insert-db-backup-integration.js +3 -4
  49. package/core/server/data/migrations/versions/2.28/3-insert-ghost-scheduler-role.js +7 -7
  50. package/core/server/data/migrations/versions/2.28/4-insert-scheduler-integration.js +3 -3
  51. package/core/server/data/migrations/versions/4.23/01-truncate-offer-names.js +58 -0
  52. package/core/server/data/schema/fixtures/fixture-manager.js +340 -0
  53. package/core/server/data/schema/fixtures/index.js +8 -2
  54. package/core/server/services/email-analytics/jobs/index.js +1 -1
  55. package/core/server/services/mega/post-email-serializer.js +5 -1
  56. package/core/server/services/mega/segment-parser.js +1 -2
  57. package/core/server/services/mega/template.js +52 -37
  58. package/core/server/services/nft-oembed.js +7 -21
  59. package/core/server/services/oembed.js +24 -24
  60. package/core/server/services/public-config/config.js +1 -1
  61. package/core/server/services/redirects/api.js +18 -23
  62. package/core/server/services/redirects/index.js +18 -10
  63. package/core/server/services/redirects/utils.js +14 -0
  64. package/core/server/services/redirects/validation.js +10 -0
  65. package/core/server/services/route-settings/index.js +40 -17
  66. package/core/server/services/route-settings/route-settings.js +127 -114
  67. package/core/server/services/route-settings/settings-loader.js +14 -32
  68. package/core/server/services/themes/activation-bridge.js +3 -3
  69. package/core/server/services/url/LocalFileCache.js +75 -0
  70. package/core/server/services/url/Resources.js +8 -2
  71. package/core/server/services/url/UrlGenerator.js +23 -20
  72. package/core/server/services/url/UrlService.js +75 -63
  73. package/core/server/services/url/index.js +17 -3
  74. package/core/server/web/admin/app.js +7 -10
  75. package/core/server/web/admin/controller.js +35 -12
  76. package/core/server/web/admin/middleware/redirect-admin-urls.js +15 -0
  77. package/core/server/web/admin/views/default-prod.html +4 -4
  78. package/core/server/web/admin/views/default.html +4 -4
  79. package/core/server/web/api/app.js +1 -1
  80. package/core/server/web/api/canary/admin/app.js +3 -6
  81. package/core/server/web/api/canary/admin/middleware.js +6 -6
  82. package/core/server/web/api/canary/admin/routes.js +5 -5
  83. package/core/server/web/api/canary/content/app.js +3 -6
  84. package/core/server/web/api/canary/content/middleware.js +3 -3
  85. package/core/server/web/api/v2/admin/app.js +3 -6
  86. package/core/server/web/api/v2/admin/middleware.js +6 -6
  87. package/core/server/web/api/v2/admin/routes.js +5 -5
  88. package/core/server/web/api/v2/content/app.js +3 -6
  89. package/core/server/web/api/v2/content/middleware.js +3 -3
  90. package/core/server/web/api/v3/admin/app.js +3 -6
  91. package/core/server/web/api/v3/admin/middleware.js +6 -6
  92. package/core/server/web/api/v3/admin/routes.js +5 -5
  93. package/core/server/web/api/v3/content/app.js +3 -6
  94. package/core/server/web/api/v3/content/middleware.js +3 -3
  95. package/core/server/web/members/app.js +6 -9
  96. package/core/server/web/oauth/app.js +0 -4
  97. package/core/server/web/parent/app.js +17 -9
  98. package/core/server/web/parent/frontend.js +1 -1
  99. package/core/server/web/shared/index.js +2 -2
  100. package/core/server/web/shared/{middlewares → middleware}/api/index.js +0 -0
  101. package/core/server/web/shared/{middlewares → middleware}/api/spam-prevention.js +0 -0
  102. package/core/server/web/shared/{middlewares → middleware}/brute.js +0 -0
  103. package/core/server/web/shared/{middlewares → middleware}/cache-control.js +0 -0
  104. package/core/server/web/shared/{middlewares → middleware}/error-handler.js +70 -53
  105. package/core/server/web/shared/{middlewares → middleware}/index.js +0 -4
  106. package/core/server/web/shared/{middlewares → middleware}/pretty-urls.js +0 -0
  107. package/core/server/web/shared/{middlewares → middleware}/uncapitalise.js +0 -0
  108. package/core/server/web/shared/{middlewares → middleware}/url-redirects.js +0 -0
  109. package/core/shared/config/defaults.json +7 -1
  110. package/core/shared/config/helpers.js +42 -0
  111. package/core/shared/config/loader.js +1 -1
  112. package/core/shared/labs.js +7 -2
  113. package/loggingrc.js +19 -20
  114. package/package.json +35 -34
  115. package/yarn.lock +822 -345
  116. package/core/server/data/schema/fixtures/utils.js +0 -321
  117. package/core/server/web/parent/vhost-utils.js +0 -39
  118. package/core/server/web/shared/middlewares/maintenance.js +0 -25
@@ -1,7 +1,7 @@
1
1
  const logging = require('@tryghost/logging');
2
2
  const merge = require('lodash/merge');
3
3
  const models = require('../../../../models');
4
- const utils = require('../../../schema/fixtures/utils');
4
+ const {fixtureManager} = require('../../../schema/fixtures');
5
5
 
6
6
  const _private = {};
7
7
 
@@ -15,12 +15,12 @@ _private.printResult = function printResult(result, message) {
15
15
 
16
16
  _private.addGhostSchedulerIntegration = (options) => {
17
17
  const message = 'Adding "Ghost Scheduler" integration';
18
- const fixtureIntegration = utils.findModelFixtureEntry('Integration', {slug: 'ghost-scheduler'});
18
+ const fixtureIntegration = fixtureManager.findModelFixtureEntry('Integration', {slug: 'ghost-scheduler'});
19
19
 
20
20
  return models.Integration.findOne({slug: fixtureIntegration.slug}, options)
21
21
  .then((integration) => {
22
22
  if (!integration) {
23
- return utils.addFixturesForModel({
23
+ return fixtureManager.addFixturesForModel({
24
24
  name: 'Integration',
25
25
  entries: [fixtureIntegration]
26
26
  }, options).then(result => _private.printResult(result, message));
@@ -0,0 +1,58 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {createTransactionalMigration} = require('../../utils');
3
+
4
+ /**
5
+ * @param {(val: string) => boolean} exists
6
+ * @param {string} requested
7
+ * @param {string} attempt
8
+ * @param {number} n
9
+ *
10
+ * @returns {string}
11
+ */
12
+ function getUnique(exists, requested, attempt = requested, n = 1) {
13
+ if (!exists(attempt)) {
14
+ return attempt;
15
+ }
16
+ const newAttempt = requested.slice(0, -n.toString().length) + n;
17
+ return getUnique(exists, requested, newAttempt, n + 1);
18
+ }
19
+
20
+ module.exports = createTransactionalMigration(
21
+ async function up(knex) {
22
+ const allOffers = await knex
23
+ .select('id', 'name')
24
+ .from('offers');
25
+
26
+ const offersNeedingTrunctation = allOffers.filter((row) => {
27
+ return row.name.length >= 40;
28
+ });
29
+
30
+ if (offersNeedingTrunctation.length === 0) {
31
+ logging.warn('No Offers found needing truncation');
32
+ return;
33
+ } else {
34
+ logging.info(`Found ${offersNeedingTrunctation.length} Offers needing truncation`);
35
+ }
36
+
37
+ const truncatedOffers = offersNeedingTrunctation.reduce((offers, row) => {
38
+ function exists(name) {
39
+ return offers.find(offer => offer.name === name) !== undefined;
40
+ }
41
+
42
+ const updatedRow = {
43
+ id: row.id,
44
+ name: getUnique(exists, row.name.slice(0, 40))
45
+ };
46
+
47
+ return offers.concat(updatedRow);
48
+ }, []);
49
+
50
+ for (const truncatedOffer of truncatedOffers) {
51
+ await knex('offers')
52
+ .update('name', truncatedOffer.name)
53
+ .where('id', truncatedOffer.id);
54
+ }
55
+ },
56
+ // no-op we've lost the data required to roll this back
57
+ async function down() {}
58
+ );
@@ -0,0 +1,340 @@
1
+ const _ = require('lodash');
2
+ const Promise = require('bluebird');
3
+ const logging = require('@tryghost/logging');
4
+ const {sequence} = require('@tryghost/promise');
5
+
6
+ const models = require('../../../models');
7
+ const baseUtils = require('../../../models/base/utils');
8
+
9
+ const moment = require('moment');
10
+
11
+ class FixtureManager {
12
+ constructor(fixtures) {
13
+ this.fixtures = fixtures;
14
+ }
15
+
16
+ /**
17
+ * ### Match Func
18
+ * Figures out how to match across various combinations of keys and values.
19
+ * Match can be a string or an array containing 2 strings
20
+ * Key and Value are the values to be found
21
+ * Value can also be an array, in which case we look for a match in the array.
22
+ * @api private
23
+ * @param {String|Array} match
24
+ * @param {String|Integer} key
25
+ * @param {String|Array} [value]
26
+ * @returns {Function} matching function
27
+ */
28
+ static matchFunc(match, key, value) {
29
+ if (_.isArray(match)) {
30
+ return function (item) {
31
+ let valueTest = true;
32
+
33
+ if (_.isArray(value)) {
34
+ valueTest = value.indexOf(item.get(match[1])) > -1;
35
+ } else if (value !== 'all') {
36
+ valueTest = item.get(match[1]) === value;
37
+ }
38
+
39
+ return item.get(match[0]) === key && valueTest;
40
+ };
41
+ }
42
+
43
+ return function (item) {
44
+ key = key === 0 && value ? value : key;
45
+ return item.get(match) === key;
46
+ };
47
+ }
48
+
49
+ static matchObj(match, item) {
50
+ const matchedObj = {};
51
+
52
+ if (_.isArray(match)) {
53
+ _.each(match, (matchProp) => {
54
+ matchedObj[matchProp] = item.get(matchProp);
55
+ });
56
+ } else {
57
+ matchedObj[match] = item.get(match);
58
+ }
59
+
60
+ return matchedObj;
61
+ }
62
+
63
+ /**
64
+ * Add All Fixtures
65
+ *
66
+ * Helper method to handle adding all fixtures
67
+ *
68
+ * @param {object} options
69
+ * @returns
70
+ */
71
+ async addAllFixtures(options) {
72
+ const localOptions = _.merge({
73
+ context: {internal: true},
74
+ migrating: true
75
+ }, options);
76
+
77
+ await Promise.mapSeries(this.fixtures.models, (model) => {
78
+ logging.info('Model: ' + model.name);
79
+
80
+ return this.addFixturesForModel(model, localOptions);
81
+ });
82
+
83
+ await Promise.mapSeries(this.fixtures.relations, (relation) => {
84
+ logging.info('Relation: ' + relation.from.model + ' to ' + relation.to.model);
85
+ return this.addFixturesForRelation(relation, localOptions);
86
+ });
87
+ }
88
+
89
+ /*
90
+ * Find methods - use the local fixtures
91
+ */
92
+
93
+ /**
94
+ * ### Find Model Fixture
95
+ * Finds a model fixture based on model name
96
+ * @api private
97
+ * @param {String} modelName
98
+ * @returns {Object} model fixture
99
+ */
100
+ findModelFixture(modelName) {
101
+ return _.find(this.fixtures.models, (modelFixture) => {
102
+ return modelFixture.name === modelName;
103
+ });
104
+ }
105
+
106
+ /**
107
+ * ### Find Model Fixture Entry
108
+ * Find a single model fixture entry by model name & a matching expression for the FIND function
109
+ * @param {String} modelName
110
+ * @param {String|Object|Function} matchExpr
111
+ * @returns {Object} model fixture entry
112
+ */
113
+ findModelFixtureEntry(modelName, matchExpr) {
114
+ return _.find(this.findModelFixture(modelName).entries, matchExpr);
115
+ }
116
+
117
+ /**
118
+ * ### Find Model Fixtures
119
+ * Find a model fixture name & a matching expression for the FILTER function
120
+ * @param {String} modelName
121
+ * @param {String|Object|Function} matchExpr
122
+ * @returns {Object} model fixture
123
+ */
124
+ findModelFixtures(modelName, matchExpr) {
125
+ const foundModel = _.cloneDeep(this.findModelFixture(modelName));
126
+ foundModel.entries = _.filter(foundModel.entries, matchExpr);
127
+ return foundModel;
128
+ }
129
+
130
+ /**
131
+ * ### Find Relation Fixture
132
+ * Find a relation fixture by from & to models
133
+ * @api private
134
+ * @param {String} from
135
+ * @param {String} to
136
+ * @returns {Object} relation fixture
137
+ */
138
+ findRelationFixture(from, to) {
139
+ return _.find(this.fixtures.relations, (relation) => {
140
+ return relation.from.model === from && relation.to.model === to;
141
+ });
142
+ }
143
+
144
+ /**
145
+ * ### Find Permission Relations For Object
146
+ * Specialist function can return the permission relation fixture with only entries for a particular object.model
147
+ * @param {String} objName
148
+ * @returns {Object} fixture relation
149
+ */
150
+ findPermissionRelationsForObject(objName, role) {
151
+ // Make a copy and delete any entries we don't want
152
+ const foundRelation = _.cloneDeep(this.findRelationFixture('Role', 'Permission'));
153
+
154
+ _.each(foundRelation.entries, (entry, key) => {
155
+ _.each(entry, (perm, obj) => {
156
+ if (obj !== objName) {
157
+ delete entry[obj];
158
+ }
159
+ });
160
+
161
+ if (_.isEmpty(entry) || (role && role !== key)) {
162
+ delete foundRelation.entries[key];
163
+ }
164
+ });
165
+
166
+ return foundRelation;
167
+ }
168
+
169
+ /******************************************************
170
+ * From here down, the methods require access to models
171
+ * But aren't dependent on this.fixtures
172
+ ******************************************************/
173
+
174
+ /**
175
+ * ### Fetch Relation Data
176
+ * Before we build relations we need to fetch all of the models from both sides so that we can
177
+ * use filter and find to quickly locate the correct models.
178
+ * @api private
179
+ * @param {{from, to, entries}} relation
180
+ * @returns {Promise<*>}
181
+ */
182
+ fetchRelationData(relation, options) {
183
+ const fromOptions = _.extend({}, options, {withRelated: [relation.from.relation]});
184
+
185
+ const props = {
186
+ from: models[relation.from.model].findAll(fromOptions),
187
+ to: models[relation.to.model].findAll(options)
188
+ };
189
+
190
+ return Promise.props(props);
191
+ }
192
+
193
+ /**
194
+ * ### Add Fixtures for Model
195
+ * Takes a model fixture, with a name and some entries and processes these
196
+ * into a sequence of promises to get each fixture added.
197
+ *
198
+ * @param {{name, entries}} modelFixture
199
+ * @returns {Promise<any>}
200
+ */
201
+ async addFixturesForModel(modelFixture, options = {}) {
202
+ // Clone the fixtures as they get changed in this function.
203
+ // The initial blog posts will be added a `published_at` property, which
204
+ // would change the fixturesHash.
205
+ modelFixture = _.cloneDeep(modelFixture);
206
+ // The Post model fixtures need a `published_at` date, where at least the seconds
207
+ // are different, otherwise `prev_post` and `next_post` helpers won't workd with
208
+ // them.
209
+ if (modelFixture.name === 'Post') {
210
+ _.forEach(modelFixture.entries, (post, index) => {
211
+ if (!post.published_at) {
212
+ post.published_at = moment().add(index, 'seconds');
213
+ }
214
+ });
215
+ }
216
+
217
+ const results = await Promise.mapSeries(modelFixture.entries, async (entry) => {
218
+ let data = {};
219
+
220
+ // CASE: if id is specified, only query by id
221
+ if (entry.id) {
222
+ data.id = entry.id;
223
+ } else if (entry.slug) {
224
+ data.slug = entry.slug;
225
+ } else {
226
+ data = _.cloneDeep(entry);
227
+ }
228
+
229
+ if (modelFixture.name === 'Post') {
230
+ data.status = 'all';
231
+ }
232
+
233
+ const found = await models[modelFixture.name].findOne(data, options);
234
+ if (!found) {
235
+ return models[modelFixture.name].add(entry, options);
236
+ }
237
+ });
238
+
239
+ return {expected: modelFixture.entries.length, done: _.compact(results).length};
240
+ }
241
+
242
+ /**
243
+ * ## Add Fixtures for Relation
244
+ * Takes a relation fixtures object, with a from, to and some entries and processes these
245
+ * into a sequence of promises, to get each fixture added.
246
+ *
247
+ * @param {{from, to, entries}} relationFixture
248
+ * @returns {Promise<any>}
249
+ */
250
+ async addFixturesForRelation(relationFixture, options) {
251
+ const ops = [];
252
+ let max = 0;
253
+
254
+ const data = await this.fetchRelationData(relationFixture, options);
255
+
256
+ _.each(relationFixture.entries, (entry, key) => {
257
+ const fromItem = data.from.find(FixtureManager.matchFunc(relationFixture.from.match, key));
258
+
259
+ // CASE: You add new fixtures e.g. a new role in a new release.
260
+ // As soon as an **older** migration script wants to add permissions for any resource, it iterates over the
261
+ // permissions for each role. But if the role does not exist yet, it won't find the matching db entry and breaks.
262
+ if (!fromItem) {
263
+ logging.warn('Skip: Target database entry not found for key: ' + key);
264
+ return Promise.resolve();
265
+ }
266
+
267
+ _.each(entry, (value, entryKey) => {
268
+ let toItems = data.to.filter(FixtureManager.matchFunc(relationFixture.to.match, entryKey, value));
269
+ max += toItems.length;
270
+
271
+ // Remove any duplicates that already exist in the collection
272
+ toItems = _.reject(toItems, (item) => {
273
+ return fromItem
274
+ .related(relationFixture.from.relation)
275
+ .find((model) => {
276
+ const objectToMatch = FixtureManager.matchObj(relationFixture.to.match, item);
277
+ return Object.keys(objectToMatch).every((keyToCheck) => {
278
+ return model.get(keyToCheck) === objectToMatch[keyToCheck];
279
+ });
280
+ });
281
+ });
282
+
283
+ if (toItems && toItems.length > 0) {
284
+ ops.push(function addRelationItems() {
285
+ return baseUtils.attach(
286
+ models[relationFixture.from.Model || relationFixture.from.model],
287
+ fromItem.id,
288
+ relationFixture.from.relation,
289
+ toItems,
290
+ options
291
+ );
292
+ });
293
+ }
294
+ });
295
+ });
296
+
297
+ const result = await sequence(ops);
298
+ return {expected: max, done: _(result).map('length').sum()};
299
+ }
300
+
301
+ async removeFixturesForModel(modelFixture, options) {
302
+ const results = await Promise.mapSeries(modelFixture.entries, async (entry) => {
303
+ const found = models[modelFixture.name].findOne(entry.id ? {id: entry.id} : entry, options);
304
+ if (found) {
305
+ return models[modelFixture.name].destroy(_.extend(options, {id: found.id}));
306
+ }
307
+ });
308
+
309
+ return {expected: modelFixture.entries.length, done: results.length};
310
+ }
311
+
312
+ async removeFixturesForRelation(relationFixture, options) {
313
+ const data = await this.fetchRelationData(relationFixture, options);
314
+ const ops = [];
315
+
316
+ _.each(relationFixture.entries, (entry, key) => {
317
+ const fromItem = data.from.find(FixtureManager.matchFunc(relationFixture.from.match, key));
318
+
319
+ _.each(entry, (value, entryKey) => {
320
+ const toItems = data.to.filter(FixtureManager.matchFunc(relationFixture.to.match, entryKey, value));
321
+
322
+ if (toItems && toItems.length > 0) {
323
+ ops.push(function detachRelation() {
324
+ return baseUtils.detach(
325
+ models[relationFixture.from.Model || relationFixture.from.model],
326
+ fromItem.id,
327
+ relationFixture.from.relation,
328
+ toItems,
329
+ options
330
+ );
331
+ });
332
+ }
333
+ });
334
+ });
335
+
336
+ return await sequence(ops);
337
+ }
338
+ }
339
+
340
+ module.exports = FixtureManager;
@@ -1,2 +1,8 @@
1
- module.exports = require('./fixtures');
2
- module.exports.utils = require('./utils');
1
+ const FixtureManager = require('./fixture-manager');
2
+ const config = require('../../../../shared/config');
3
+
4
+ const fixturePath = config.get('paths').fixtures;
5
+ const fixtures = require(fixturePath);
6
+
7
+ module.exports.FixtureManager = FixtureManager;
8
+ module.exports.fixtureManager = new FixtureManager(fixtures);
@@ -12,7 +12,7 @@ module.exports = {
12
12
  !hasScheduled &&
13
13
  config.get('emailAnalytics') &&
14
14
  config.get('backgroundJobs:emailAnalytics') &&
15
- !process.env.NODE_ENV.match(/^testing/)
15
+ !process.env.NODE_ENV.startsWith('test')
16
16
  ) {
17
17
  // Don't register email analytics job if we have no emails,
18
18
  // processor usage from many sites spinning up threads can be high.
@@ -3,7 +3,6 @@ const template = require('./template');
3
3
  const settingsCache = require('../../../shared/settings-cache');
4
4
  const urlUtils = require('../../../shared/url-utils');
5
5
  const moment = require('moment-timezone');
6
- const cheerio = require('cheerio');
7
6
  const api = require('../../api');
8
7
  const {URL} = require('url');
9
8
  const mobiledocLib = require('../../lib/mobiledoc');
@@ -24,6 +23,8 @@ const formatHtmlForEmail = function formatHtmlForEmail(html) {
24
23
 
25
24
  // convert juiced HTML to a DOM-like interface for further manipulation
26
25
  // happens after inlining of CSS so we can change element types without worrying about styling
26
+
27
+ const cheerio = require('cheerio');
27
28
  const _cheerio = cheerio.load(juicedHtml);
28
29
 
29
30
  // force all links to open in new tab
@@ -243,6 +244,7 @@ const serialize = async (postModel, options = {isBrowserPreview: false, apiVersi
243
244
 
244
245
  // perform any email specific adjustments to the mobiledoc->HTML render output
245
246
  // body wrapper is required so we can get proper top-level selections
247
+ const cheerio = require('cheerio');
246
248
  let _cheerio = cheerio.load(`<body>${post.html}</body>`);
247
249
  // remove leading/trailing HRs
248
250
  _cheerio(`
@@ -311,6 +313,8 @@ const serialize = async (postModel, options = {isBrowserPreview: false, apiVersi
311
313
  };
312
314
 
313
315
  function renderEmailForSegment(email, memberSegment) {
316
+ const cheerio = require('cheerio');
317
+
314
318
  const result = {...email};
315
319
  const $ = cheerio.load(result.html);
316
320
 
@@ -1,6 +1,5 @@
1
- const cheerio = require('cheerio');
2
-
3
1
  const getSegmentsFromHtml = (html) => {
2
+ const cheerio = require('cheerio');
4
3
  const $ = cheerio.load(html);
5
4
 
6
5
  const allSegments = $('[data-gh-segment]')
@@ -544,60 +544,75 @@ figure blockquote p {
544
544
  border-width: 0.8em 0 0.8em 1.5em;
545
545
  }
546
546
 
547
- a.kg-nft-card {
547
+ .kg-nft-link {
548
+ display: block;
549
+ text-decoration: none !important;
550
+ color: #15212A !important;
551
+ font-family: inherit !important;
552
+ font-size: 14px;
553
+ line-height: 1.3em;
554
+ padding-top: 4px;
555
+ padding-right: 20px;
556
+ padding-left: 20px;
557
+ padding-bottom: 4px;
558
+ }
559
+
560
+ .kg-card-callout {
548
561
  display: flex;
549
- flex-direction: column;
550
- color: #15212A;
551
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
552
- font-size: 15px;
553
- text-decoration: none;
562
+ margin: 0 0 1.5em 0;
563
+ padding: 20px 28px;
554
564
  border-radius: 3px;
555
- border: 1px solid #e5eff5;
556
- width: 100%;
557
- max-width: 512px;
558
- color: #222;
565
+ }
566
+
567
+ .kg-card-callout p {
568
+ margin: 0
569
+ }
570
+
571
+ .kg-card-callout-grey {
572
+ background: #eef0f2;
573
+ }
574
+
575
+ .kg-card-callout-white {
559
576
  background: #fff;
560
- margin: 0 auto;
577
+ box-shadow: inset 0 0 0 1px #dddedf;
561
578
  }
562
579
 
563
- .kg-nft-metadata {
564
- padding: 20px;
580
+ .kg-card-callout-blue {
581
+ background: #E9F6FB;
565
582
  }
566
583
 
567
- .kg-nft-image {
568
- border-radius: 2px 2px 0 0;
584
+ .kg-card-callout-green {
585
+ background: #E8F8EA;
569
586
  }
570
587
 
571
- .kg-nft-header {
572
- display: flex;
573
- justify-content: space-between;
574
- gap: 20px;
588
+ .kg-card-callout-yellow {
589
+ background: #FCF4E3;
575
590
  }
576
591
 
577
- .kg-nft-title {
578
- font-family: inherit;
579
- font-size: 19px;
580
- font-weight: 700;
581
- margin: 0;
582
- color: #222;
592
+ .kg-card-callout-red {
593
+ background: #FBE9E9;
583
594
  }
584
595
 
585
- .kg-nft-creator {
586
- font-family: inherit;
587
- margin: 4px 0 0;
588
- color: #ababab;
596
+ .kg-card-callout-pink {
597
+ background: #FCEEF8;
589
598
  }
590
599
 
591
- .kg-nft-creator-name {
592
- font-weight: 500;
593
- color: #222;
600
+ .kg-card-callout-purple {
601
+ background: #F2EDFC;
594
602
  }
595
603
 
596
- .kg-nft-description {
597
- font-family: inherit;
598
- line-height: 1.4em;
599
- margin: 12px 0 0;
600
- color: #222;
604
+ .kg-card-callout-accent {
605
+ background: ${templateSettings.accentColor || '#15212A'};
606
+ color: #fff;
607
+ }
608
+
609
+ .kg-card-callout-accent a {
610
+ color: #fff;
611
+ }
612
+
613
+ .kg-callout-emoji {
614
+ padding-right: 12px;
615
+ font-size: 20px;
601
616
  }
602
617
 
603
618
  /* -------------------------------------
@@ -28,42 +28,28 @@ class NFTOEmbedProvider {
28
28
  * @param {URL} url
29
29
  * @param {IExternalRequest} externalRequest
30
30
  *
31
- * @returns {Promise<import('oembed-parser').RichTypeData & Object<string, any>>}
31
+ * @returns {Promise<object>}
32
32
  */
33
33
  async getOEmbedData(url, externalRequest) {
34
34
  const [match, transaction, asset] = url.pathname.match(OPENSEA_PATH_REGEX);
35
35
  if (!match) {
36
36
  return null;
37
37
  }
38
- const result = await externalRequest(`https://api.opensea.io/api/v1/asset/${transaction}/${asset}/`, {
38
+ const result = await externalRequest(`https://api.opensea.io/api/v1/asset/${transaction}/${asset}/?format=json`, {
39
39
  json: true
40
40
  });
41
41
  return {
42
42
  version: '1.0',
43
- type: 'rich',
43
+ type: 'nft',
44
44
  title: result.body.name,
45
45
  author_name: result.body.creator.user.username,
46
46
  author_url: `https://opensea.io/${result.body.creator.user.username}`,
47
47
  provider_name: 'OpenSea',
48
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 ? `&bull; ${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
49
+ image_url: result.body.image_url,
50
+ creator_name: result.body.creator.user.username,
51
+ description: result.body.description,
52
+ collection_name: result.body.collection.name
67
53
  };
68
54
  }
69
55
  }