ghost 4.19.1 → 4.20.3

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 (139) hide show
  1. package/.eslintrc.js +9 -8
  2. package/Gruntfile.js +1 -1
  3. package/PRIVACY.md +3 -0
  4. package/content/adapters/README.md +2 -2
  5. package/core/boot.js +4 -4
  6. package/core/bridge.js +9 -1
  7. package/core/built/assets/{chunk.3.0778d8e4d707d2a625f1.js → chunk.3.777d43e2ce954ba8b2f5.js} +1 -1
  8. package/core/built/assets/codemirror/{codemirror-21a09582262987037db73b152fb35f7c.js → codemirror-d25c379b87ec8b33d54ac7149bc0b6ae.js} +14 -14
  9. package/core/built/assets/ghost-dark-20e2892d4f30d0d1183c9ac725ea37d0.css +1 -0
  10. package/core/built/assets/{ghost.min-102753ec485602c8fe80d60a1750bf84.js → ghost.min-07b6a50c54b3e2e190332c28c7255d2f.js} +525 -340
  11. package/core/built/assets/ghost.min-57e46fd3b1145ecf2cbd185a13611f3b.css +1 -0
  12. package/core/built/assets/icons/arrow-left-small.svg +0 -4
  13. package/core/built/assets/img/footer-marketplace-bg-572b6c6486a7e26316954d599eaa9f30.png +0 -0
  14. package/core/built/assets/img/marketing/offers-1-f2e1b653c4d5bb90eea9d7a2862530f9.jpg +0 -0
  15. package/core/built/assets/img/marketing/offers-2-28a225d34cc39d133748431536961d00.jpg +0 -0
  16. package/core/built/assets/img/marketing/offers-3-2094c91ab21a16c37fbe6ec16c140160.jpg +0 -0
  17. package/core/built/assets/img/themes/Casper-c7e784d7188cc5d7f097d9b6c97b0263.jpg +0 -0
  18. package/core/built/assets/simplemde/{simplemde-232f69d126310434489071a1891e6d8b.js → simplemde-3ffc0ec9e9fecf29b9a499db678c9e65.js} +14 -14
  19. package/core/built/assets/{vendor.min-0916203b598271a795909e8e0b1c16c2.js → vendor.min-af502ac4142871500fc424f6a5a254ec.js} +1046 -1043
  20. package/core/frontend/apps/amp/lib/router.js +1 -1
  21. package/core/frontend/meta/author-url.js +1 -1
  22. package/core/frontend/meta/url.js +1 -1
  23. package/core/{server → frontend}/public/favicon.ico +0 -0
  24. package/core/{server → frontend}/public/ghost.css +0 -0
  25. package/core/{server → frontend}/public/ghost.min.css +0 -0
  26. package/core/{server → frontend}/public/robots.txt +0 -0
  27. package/core/{server → frontend}/public/sitemap.xsl +0 -0
  28. package/core/frontend/services/proxy.js +1 -1
  29. package/core/frontend/services/routing/CollectionRouter.js +3 -49
  30. package/core/frontend/services/routing/ParentRouter.js +1 -4
  31. package/core/frontend/services/routing/StaticPagesRouter.js +3 -5
  32. package/core/frontend/services/routing/StaticRoutesRouter.js +4 -6
  33. package/core/frontend/services/routing/TaxonomyRouter.js +4 -5
  34. package/core/frontend/services/routing/controllers/collection.js +2 -2
  35. package/core/frontend/services/routing/controllers/email-post.js +2 -2
  36. package/core/frontend/services/routing/controllers/entry.js +2 -2
  37. package/core/frontend/services/routing/controllers/preview.js +2 -2
  38. package/core/frontend/services/routing/index.js +6 -12
  39. package/core/frontend/services/routing/registry.js +13 -0
  40. package/core/frontend/services/routing/router-manager.js +185 -0
  41. package/core/frontend/services/rss/generate-feed.js +2 -2
  42. package/core/frontend/services/theme-engine/i18n/i18n.js +267 -28
  43. package/core/frontend/services/theme-engine/i18n/index.js +1 -1
  44. package/core/frontend/services/theme-engine/i18n/theme-i18n.js +73 -0
  45. package/core/frontend/web/index.js +1 -0
  46. package/core/{server/web/site → frontend/web}/middleware/handle-image-sizes.js +4 -4
  47. package/core/{server/web/site → frontend/web}/middleware/index.js +0 -0
  48. package/core/{server/web/site → frontend/web}/middleware/redirect-ghost-to-admin.js +3 -3
  49. package/core/{server/web/site → frontend/web}/middleware/serve-favicon.js +6 -6
  50. package/core/{server/web/site → frontend/web}/middleware/serve-public-file.js +2 -2
  51. package/core/{server/web/site → frontend/web}/middleware/static-theme.js +3 -3
  52. package/core/{server/web/site → frontend/web}/routes.js +5 -4
  53. package/core/{server/web/site/app.js → frontend/web/site.js} +12 -16
  54. package/core/server/adapters/storage/LocalFileStorage.js +35 -39
  55. package/core/server/adapters/storage/index.js +12 -2
  56. package/core/server/api/canary/images.js +1 -1
  57. package/core/server/api/canary/offers.js +19 -0
  58. package/core/server/api/canary/utils/serializers/output/settings.js +2 -3
  59. package/core/server/api/canary/utils/serializers/output/utils/url.js +1 -1
  60. package/core/server/api/v2/images.js +1 -1
  61. package/core/server/api/v2/utils/serializers/output/utils/url.js +1 -1
  62. package/core/server/api/v3/images.js +1 -1
  63. package/core/server/api/v3/utils/serializers/output/settings.js +2 -3
  64. package/core/server/api/v3/utils/serializers/output/utils/url.js +1 -1
  65. package/core/server/data/importer/handlers/image.js +1 -1
  66. package/core/server/data/importer/importers/image.js +1 -1
  67. package/core/server/data/migrations/init/1-create-tables.js +7 -8
  68. package/core/server/data/migrations/init/2-create-fixtures.js +8 -8
  69. package/core/server/data/migrations/versions/4.20/01-remove-offer-redemptions-table.js +19 -0
  70. package/core/server/data/migrations/versions/4.20/02-remove-offers-table.js +30 -0
  71. package/core/server/data/migrations/versions/4.20/03-add-offers-table.js +21 -0
  72. package/core/server/data/migrations/versions/4.20/04-add-offer-redemptions-table.js +9 -0
  73. package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +41 -0
  74. package/core/server/data/schema/fixtures/utils.js +150 -143
  75. package/core/server/data/schema/schema.js +4 -3
  76. package/core/server/frontend/ghost.min.css +1 -0
  77. package/core/server/lib/image/image-size.js +2 -2
  78. package/core/server/lib/mobiledoc.js +3 -2
  79. package/core/server/models/action.js +7 -4
  80. package/core/server/models/base/plugins/overrides.js +19 -6
  81. package/core/server/models/index.js +4 -46
  82. package/core/server/models/member.js +5 -0
  83. package/core/server/models/user.js +2 -1
  84. package/core/server/overrides.js +6 -2
  85. package/core/server/services/adapter-manager/config.js +1 -0
  86. package/core/server/services/adapter-manager/index.js +9 -5
  87. package/core/server/services/adapter-manager/options-resolver.js +18 -0
  88. package/core/server/services/bulk-email/mailgun.js +1 -1
  89. package/core/server/services/mega/post-email-serializer.js +2 -2
  90. package/core/server/services/members/api.js +1 -3
  91. package/core/server/services/members/emails/signin.js +1 -1
  92. package/core/server/services/members/emails/signup.js +1 -1
  93. package/core/server/services/members/emails/subscribe.js +1 -1
  94. package/core/server/services/members/service.js +2 -1
  95. package/core/server/services/offers/service.js +1 -1
  96. package/core/server/services/route-settings/route-settings.js +1 -1
  97. package/core/server/services/settings/index.js +3 -1
  98. package/core/server/services/settings/settings-bread-service.js +42 -20
  99. package/core/server/services/slack.js +1 -1
  100. package/core/server/services/themes/activate.js +2 -2
  101. package/core/server/services/themes/activation-bridge.js +6 -6
  102. package/core/server/services/themes/storage.js +1 -1
  103. package/core/{frontend → server}/services/url/Queue.js +0 -0
  104. package/core/{frontend → server}/services/url/Resource.js +0 -0
  105. package/core/{frontend → server}/services/url/Resources.js +2 -2
  106. package/core/{frontend → server}/services/url/UrlGenerator.js +14 -14
  107. package/core/{frontend → server}/services/url/UrlService.js +12 -15
  108. package/core/{frontend → server}/services/url/Urls.js +1 -1
  109. package/core/{frontend → server}/services/url/configs/canary.js +0 -0
  110. package/core/{frontend → server}/services/url/configs/v2.js +0 -0
  111. package/core/{frontend → server}/services/url/configs/v3.js +0 -0
  112. package/core/{frontend → server}/services/url/configs/v4.js +0 -0
  113. package/core/{frontend → server}/services/url/index.js +0 -0
  114. package/core/server/services/xmlrpc.js +1 -1
  115. package/core/server/update-check.js +3 -3
  116. package/core/server/web/admin/controller.js +11 -0
  117. package/core/server/web/admin/views/default-prod.html +4 -4
  118. package/core/server/web/admin/views/default.html +4 -4
  119. package/core/server/web/api/app.js +8 -9
  120. package/core/server/web/oauth/app.js +4 -2
  121. package/core/server/web/parent/backend.js +3 -3
  122. package/core/server/web/parent/frontend.js +2 -2
  123. package/core/server/web/shared/middlewares/custom-redirects.js +0 -8
  124. package/core/server/web/shared/middlewares/maintenance.js +1 -1
  125. package/core/server/web/well-known.js +10 -10
  126. package/core/shared/config/overrides.json +1 -1
  127. package/core/shared/express.js +10 -0
  128. package/core/shared/html-to-plaintext.js +2 -2
  129. package/core/shared/labs.js +14 -5
  130. package/package.json +45 -43
  131. package/yarn.lock +649 -284
  132. package/core/built/assets/ghost-dark-da8e8eba130fb52f97494e51850d1045.css +0 -1
  133. package/core/built/assets/ghost.min-0d8f19623e9f077351bce453034daf4d.css +0 -1
  134. package/core/frontend/services/routing/bootstrap.js +0 -134
  135. package/core/server/public/404-ghost.png +0 -0
  136. package/core/server/public/404-ghost@2x.png +0 -0
  137. package/core/server/web/site/index.js +0 -1
  138. package/core/shared/i18n/i18n.js +0 -312
  139. package/core/shared/i18n/index.js +0 -6
@@ -47,7 +47,7 @@ const matchObj = function matchObj(match, item) {
47
47
  const matchedObj = {};
48
48
 
49
49
  if (_.isArray(match)) {
50
- _.each(match, function (matchProp) {
50
+ _.each(match, (matchProp) => {
51
51
  matchedObj[matchProp] = item.get(matchProp);
52
52
  });
53
53
  } else {
@@ -76,115 +76,9 @@ const fetchRelationData = function fetchRelationData(relation, options) {
76
76
  return Promise.props(props);
77
77
  };
78
78
 
79
- /**
80
- * ### Add Fixtures for Model
81
- * Takes a model fixture, with a name and some entries and processes these
82
- * into a sequence of promises to get each fixture added.
83
- *
84
- * @param {{name, entries}} modelFixture
85
- * @returns {Promise.<*>}
79
+ /*
80
+ * Find methods - use the local fixtures
86
81
  */
87
- const addFixturesForModel = function addFixturesForModel(modelFixture, options = {}) {
88
- // Clone the fixtures as they get changed in this function.
89
- // The initial blog posts will be added a `published_at` property, which
90
- // would change the fixturesHash.
91
- modelFixture = _.cloneDeep(modelFixture);
92
- // The Post model fixtures need a `published_at` date, where at least the seconds
93
- // are different, otherwise `prev_post` and `next_post` helpers won't workd with
94
- // them.
95
- if (modelFixture.name === 'Post') {
96
- _.forEach(modelFixture.entries, function (post, index) {
97
- if (!post.published_at) {
98
- post.published_at = moment().add(index, 'seconds');
99
- }
100
- });
101
- }
102
-
103
- return Promise.mapSeries(modelFixture.entries, function (entry) {
104
- let data = {};
105
-
106
- // CASE: if id is specified, only query by id
107
- if (entry.id) {
108
- data.id = entry.id;
109
- } else if (entry.slug) {
110
- data.slug = entry.slug;
111
- } else {
112
- data = _.cloneDeep(entry);
113
- }
114
-
115
- if (modelFixture.name === 'Post') {
116
- data.status = 'all';
117
- }
118
-
119
- return models[modelFixture.name].findOne(data, options).then(function (found) {
120
- if (!found) {
121
- return models[modelFixture.name].add(entry, options);
122
- }
123
- });
124
- }).then(function (results) {
125
- return {expected: modelFixture.entries.length, done: _.compact(results).length};
126
- });
127
- };
128
-
129
- /**
130
- * ## Add Fixtures for Relation
131
- * Takes a relation fixtures object, with a from, to and some entries and processes these
132
- * into a sequence of promises, to get each fixture added.
133
- *
134
- * @param {{from, to, entries}} relationFixture
135
- * @returns {Promise.<*>}
136
- */
137
- const addFixturesForRelation = function addFixturesForRelation(relationFixture, options) {
138
- const ops = [];
139
- let max = 0;
140
-
141
- return fetchRelationData(relationFixture, options).then(function getRelationOps(data) {
142
- _.each(relationFixture.entries, function processEntries(entry, key) {
143
- const fromItem = data.from.find(matchFunc(relationFixture.from.match, key));
144
-
145
- // CASE: You add new fixtures e.g. a new role in a new release.
146
- // As soon as an **older** migration script wants to add permissions for any resource, it iterates over the
147
- // permissions for each role. But if the role does not exist yet, it won't find the matching db entry and breaks.
148
- if (!fromItem) {
149
- logging.warn('Skip: Target database entry not found for key: ' + key);
150
- return Promise.resolve();
151
- }
152
-
153
- _.each(entry, function processEntryValues(value, entryKey) {
154
- let toItems = data.to.filter(matchFunc(relationFixture.to.match, entryKey, value));
155
- max += toItems.length;
156
-
157
- // Remove any duplicates that already exist in the collection
158
- toItems = _.reject(toItems, function (item) {
159
- return fromItem
160
- .related(relationFixture.from.relation)
161
- .find((model) => {
162
- const objectToMatch = matchObj(relationFixture.to.match, item);
163
- return Object.keys(objectToMatch).every(function (keyToCheck) {
164
- return model.get(keyToCheck) === objectToMatch[keyToCheck];
165
- });
166
- });
167
- });
168
-
169
- if (toItems && toItems.length > 0) {
170
- ops.push(function addRelationItems() {
171
- return baseUtils.attach(
172
- models[relationFixture.from.Model || relationFixture.from.model],
173
- fromItem.id,
174
- relationFixture.from.relation,
175
- toItems,
176
- options
177
- );
178
- });
179
- }
180
- });
181
- });
182
-
183
- return sequence(ops);
184
- }).then(function (result) {
185
- return {expected: max, done: _(result).map('length').sum()};
186
- });
187
- };
188
82
 
189
83
  /**
190
84
  * ### Find Model Fixture
@@ -194,7 +88,7 @@ const addFixturesForRelation = function addFixturesForRelation(relationFixture,
194
88
  * @returns {Object} model fixture
195
89
  */
196
90
  const findModelFixture = function findModelFixture(modelName) {
197
- return _.find(fixtures.models, function (modelFixture) {
91
+ return _.find(fixtures.models, (modelFixture) => {
198
92
  return modelFixture.name === modelName;
199
93
  });
200
94
  };
@@ -232,7 +126,7 @@ const findModelFixtures = function findModelFixtures(modelName, matchExpr) {
232
126
  * @returns {Object} relation fixture
233
127
  */
234
128
  const findRelationFixture = function findRelationFixture(from, to) {
235
- return _.find(fixtures.relations, function (relation) {
129
+ return _.find(fixtures.relations, (relation) => {
236
130
  return relation.from.model === from && relation.to.model === to;
237
131
  });
238
132
  };
@@ -247,8 +141,8 @@ const findPermissionRelationsForObject = function findPermissionRelationsForObje
247
141
  // Make a copy and delete any entries we don't want
248
142
  const foundRelation = _.cloneDeep(findRelationFixture('Role', 'Permission'));
249
143
 
250
- _.each(foundRelation.entries, function (entry, key) {
251
- _.each(entry, function (perm, obj) {
144
+ _.each(foundRelation.entries, (entry, key) => {
145
+ _.each(entry, (perm, obj) => {
252
146
  if (obj !== objName) {
253
147
  delete entry[obj];
254
148
  }
@@ -262,53 +156,166 @@ const findPermissionRelationsForObject = function findPermissionRelationsForObje
262
156
  return foundRelation;
263
157
  };
264
158
 
265
- const removeFixturesForModel = function removeFixturesForModel(modelFixture, options) {
266
- return Promise.mapSeries(modelFixture.entries, function (entry) {
267
- return models[modelFixture.name].findOne(entry.id ? {id: entry.id} : entry, options).then(function (found) {
268
- if (found) {
269
- return models[modelFixture.name].destroy(_.extend(options, {id: found.id}));
159
+ /*
160
+ * Add and Remove Functions, require access to models
161
+ */
162
+
163
+ /**
164
+ * ### Add Fixtures for Model
165
+ * Takes a model fixture, with a name and some entries and processes these
166
+ * into a sequence of promises to get each fixture added.
167
+ *
168
+ * @param {{name, entries}} modelFixture
169
+ * @returns {Promise<any>}
170
+ */
171
+ const addFixturesForModel = async function addFixturesForModel(modelFixture, options = {}) {
172
+ // Clone the fixtures as they get changed in this function.
173
+ // The initial blog posts will be added a `published_at` property, which
174
+ // would change the fixturesHash.
175
+ modelFixture = _.cloneDeep(modelFixture);
176
+ // The Post model fixtures need a `published_at` date, where at least the seconds
177
+ // are different, otherwise `prev_post` and `next_post` helpers won't workd with
178
+ // them.
179
+ if (modelFixture.name === 'Post') {
180
+ _.forEach(modelFixture.entries, (post, index) => {
181
+ if (!post.published_at) {
182
+ post.published_at = moment().add(index, 'seconds');
270
183
  }
271
184
  });
272
- }).then(function (results) {
273
- return {expected: modelFixture.entries.length, done: results.length};
185
+ }
186
+
187
+ const results = await Promise.mapSeries(modelFixture.entries, async (entry) => {
188
+ let data = {};
189
+
190
+ // CASE: if id is specified, only query by id
191
+ if (entry.id) {
192
+ data.id = entry.id;
193
+ } else if (entry.slug) {
194
+ data.slug = entry.slug;
195
+ } else {
196
+ data = _.cloneDeep(entry);
197
+ }
198
+
199
+ if (modelFixture.name === 'Post') {
200
+ data.status = 'all';
201
+ }
202
+
203
+ const found = await models[modelFixture.name].findOne(data, options);
204
+ if (!found) {
205
+ return models[modelFixture.name].add(entry, options);
206
+ }
274
207
  });
208
+
209
+ return {expected: modelFixture.entries.length, done: _.compact(results).length};
275
210
  };
276
211
 
277
- const removeFixturesForRelation = function removeFixturesForRelation(relationFixture, options) {
278
- return fetchRelationData(relationFixture, options).then(function getRelationOps(data) {
279
- const ops = [];
280
-
281
- _.each(relationFixture.entries, function processEntries(entry, key) {
282
- const fromItem = data.from.find(matchFunc(relationFixture.from.match, key));
283
-
284
- _.each(entry, function processEntryValues(value, entryKey) {
285
- const toItems = data.to.filter(matchFunc(relationFixture.to.match, entryKey, value));
286
-
287
- if (toItems && toItems.length > 0) {
288
- ops.push(function detachRelation() {
289
- return baseUtils.detach(
290
- models[relationFixture.from.Model || relationFixture.from.model],
291
- fromItem.id,
292
- relationFixture.from.relation,
293
- toItems,
294
- options
295
- );
212
+ /**
213
+ * ## Add Fixtures for Relation
214
+ * Takes a relation fixtures object, with a from, to and some entries and processes these
215
+ * into a sequence of promises, to get each fixture added.
216
+ *
217
+ * @param {{from, to, entries}} relationFixture
218
+ * @returns {Promise<any>}
219
+ */
220
+ const addFixturesForRelation = async function addFixturesForRelation(relationFixture, options) {
221
+ const ops = [];
222
+ let max = 0;
223
+
224
+ const data = await fetchRelationData(relationFixture, options);
225
+
226
+ _.each(relationFixture.entries, (entry, key) => {
227
+ const fromItem = data.from.find(matchFunc(relationFixture.from.match, key));
228
+
229
+ // CASE: You add new fixtures e.g. a new role in a new release.
230
+ // As soon as an **older** migration script wants to add permissions for any resource, it iterates over the
231
+ // permissions for each role. But if the role does not exist yet, it won't find the matching db entry and breaks.
232
+ if (!fromItem) {
233
+ logging.warn('Skip: Target database entry not found for key: ' + key);
234
+ return Promise.resolve();
235
+ }
236
+
237
+ _.each(entry, (value, entryKey) => {
238
+ let toItems = data.to.filter(matchFunc(relationFixture.to.match, entryKey, value));
239
+ max += toItems.length;
240
+
241
+ // Remove any duplicates that already exist in the collection
242
+ toItems = _.reject(toItems, (item) => {
243
+ return fromItem
244
+ .related(relationFixture.from.relation)
245
+ .find((model) => {
246
+ const objectToMatch = matchObj(relationFixture.to.match, item);
247
+ return Object.keys(objectToMatch).every((keyToCheck) => {
248
+ return model.get(keyToCheck) === objectToMatch[keyToCheck];
249
+ });
296
250
  });
297
- }
298
251
  });
252
+
253
+ if (toItems && toItems.length > 0) {
254
+ ops.push(function addRelationItems() {
255
+ return baseUtils.attach(
256
+ models[relationFixture.from.Model || relationFixture.from.model],
257
+ fromItem.id,
258
+ relationFixture.from.relation,
259
+ toItems,
260
+ options
261
+ );
262
+ });
263
+ }
299
264
  });
265
+ });
266
+
267
+ const result = await sequence(ops);
268
+ return {expected: max, done: _(result).map('length').sum()};
269
+ };
300
270
 
301
- return sequence(ops);
271
+ const removeFixturesForModel = async function removeFixturesForModel(modelFixture, options) {
272
+ const results = await Promise.mapSeries(modelFixture.entries, async (entry) => {
273
+ const found = models[modelFixture.name].findOne(entry.id ? {id: entry.id} : entry, options);
274
+ if (found) {
275
+ return models[modelFixture.name].destroy(_.extend(options, {id: found.id}));
276
+ }
302
277
  });
278
+
279
+ return {expected: modelFixture.entries.length, done: results.length};
280
+ };
281
+
282
+ const removeFixturesForRelation = async function removeFixturesForRelation(relationFixture, options) {
283
+ const data = await fetchRelationData(relationFixture, options);
284
+ const ops = [];
285
+
286
+ _.each(relationFixture.entries, (entry, key) => {
287
+ const fromItem = data.from.find(matchFunc(relationFixture.from.match, key));
288
+
289
+ _.each(entry, (value, entryKey) => {
290
+ const toItems = data.to.filter(matchFunc(relationFixture.to.match, entryKey, value));
291
+
292
+ if (toItems && toItems.length > 0) {
293
+ ops.push(function detachRelation() {
294
+ return baseUtils.detach(
295
+ models[relationFixture.from.Model || relationFixture.from.model],
296
+ fromItem.id,
297
+ relationFixture.from.relation,
298
+ toItems,
299
+ options
300
+ );
301
+ });
302
+ }
303
+ });
304
+ });
305
+
306
+ return await sequence(ops);
303
307
  };
304
308
 
305
309
  module.exports = {
306
- addFixturesForModel: addFixturesForModel,
307
- addFixturesForRelation: addFixturesForRelation,
310
+ // find
308
311
  findModelFixtureEntry: findModelFixtureEntry,
309
312
  findModelFixtures: findModelFixtures,
310
313
  findRelationFixture: findRelationFixture,
311
314
  findPermissionRelationsForObject: findPermissionRelationsForObject,
315
+
316
+ // add / remove
317
+ addFixturesForModel: addFixturesForModel,
318
+ addFixturesForRelation: addFixturesForRelation,
312
319
  removeFixturesForModel: removeFixturesForModel,
313
320
  removeFixturesForRelation: removeFixturesForRelation
314
321
  };
@@ -390,14 +390,14 @@ module.exports = {
390
390
  name: {type: 'string', maxlength: 191, nullable: false, unique: true},
391
391
  code: {type: 'string', maxlength: 191, nullable: false, unique: true},
392
392
  product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id'},
393
- stripe_coupon_id: {type: 'string', maxlength: 255, nullable: false, unique: true},
393
+ stripe_coupon_id: {type: 'string', maxlength: 255, nullable: true, unique: true},
394
394
  interval: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['month', 'year']]}},
395
395
  currency: {type: 'string', maxlength: 50, nullable: true},
396
396
  discount_type: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['percent', 'amount']]}},
397
397
  discount_amount: {type: 'integer', nullable: false},
398
398
  duration: {type: 'string', maxlength: 50, nullable: false},
399
399
  duration_in_months: {type: 'integer', nullable: true},
400
- portal_title: {type: 'string', maxlength: 191, nullable: false},
400
+ portal_title: {type: 'string', maxlength: 191, nullable: true},
401
401
  portal_description: {type: 'string', maxlength: 2000, nullable: true},
402
402
  created_at: {type: 'dateTime', nullable: false},
403
403
  updated_at: {type: 'dateTime', nullable: true}
@@ -533,7 +533,8 @@ module.exports = {
533
533
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
534
534
  offer_id: {type: 'string', maxlength: 24, nullable: false, references: 'offers.id', cascadeDelete: true},
535
535
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
536
- subscription_id: {type: 'string', maxlength: 24, nullable: false, references: 'members_stripe_customers_subscriptions.id'}
536
+ subscription_id: {type: 'string', maxlength: 24, nullable: false, references: 'members_stripe_customers_subscriptions.id', cascadeDelete: true},
537
+ created_at: {type: 'dateTime', nullable: false}
537
538
  },
538
539
  members_subscribe_events: {
539
540
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
@@ -0,0 +1 @@
1
+ /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;font-family:sans-serif}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.darkgrey{color:#343f44}.midgrey{color:#738a94}.lightgrey{color:#e5eff5}.blue{color:#3eb0ef}.red{color:#f05230}.orange{color:#fecd35}.green{color:#a4d037}.darkgrey-hover:hover{color:#343f44}.midgrey-hover:hover{color:#738a94}.lightgrey-hover:hover{color:#e5eff5}.blue-hover:hover{color:#3eb0ef}.red-hover:hover{color:#f05230}.orange-hover:hover{color:#fecd35}.green-hover:hover{color:#a4d037}*,:after,:before{box-sizing:border-box}html{-webkit-tap-highlight-color:rgba(0,0,0,0);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:62.5%;letter-spacing:.2px;line-height:1.65;overflow:hidden}body,html{height:100%;width:100%}body{color:#343f44;font-size:1.4rem;overflow:auto;overflow-x:hidden}.gh-view{-ms-flex-positive:1;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;flex-grow:1}h1,h2{text-rendering:optimizeLegibility;color:#343f44;font-size:2.9rem;line-height:1.15em;margin:0 0 .3em;text-indent:-1px}@media (max-width:500px){h1{font-size:2.4rem}}.gh-input{-webkit-appearance:none;border:1px solid #d6e3eb;border-radius:4px;color:#4b5b62;display:block;font-size:1.6rem;font-weight:300;height:40px;line-height:1em;padding:10px 12px;transition:border-color .15s linear;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;width:100%}.gh-input:focus{border-color:#b4cbda;outline:0}.gh-btn{fill:#829aa8;-webkit-font-smoothing:subpixel-antialiased;border:1px solid #d6e3eb;border-radius:5px;color:#829aa8;display:inline-block;outline:none;text-decoration:none!important;text-shadow:0 1px 0 #fff;transition:all .2s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.gh-btn span{border-radius:4px;display:block;font-size:1.3rem;font-weight:400;height:33px;letter-spacing:.2px;line-height:33px;padding:0 12px;text-align:center}.gh-btn:hover{border-color:#b4cbda}.gh-btn-hover-blue:hover{border-color:#3eb0ef;color:#3eb0ef}.gh-btn-blue{fill:#fff;background:linear-gradient(#3da1d6,#2288bf);border:0;box-shadow:0 1px 0 rgba(0,0,0,.12);color:#fff;padding:1px;text-shadow:0 -1px 0 rgba(0,0,0,.1);transition:none!important}.gh-btn-blue span{background:linear-gradient(#4ab6f0,#2fa5e4 60%,#2fa5e4 90%,#38a9e5);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.gh-btn-blue:active,.gh-btn-blue:focus{background:#1e78a9}.gh-btn-blue:active span,.gh-btn-blue:focus span{background:#29a0e0;box-shadow:none}.gh-btn-block{display:block;width:100%}.gh-input-icon{display:block;position:relative}.gh-input-icon svg{fill:color(var(--midgrey) l(15%));height:14px;left:10px;position:absolute;top:50%;transform:translateY(-7px);width:auto;z-index:2}.gh-input-icon input{padding-left:35px}.gh-app{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;height:100%;overflow:hidden}.gh-viewport{max-height:100%;overflow:hidden}.gh-main,.gh-viewport{-ms-flex-positive:1;display:-ms-flexbox;display:flex;flex-grow:1}.gh-main{background:#fff;overflow-y:auto;position:relative}.gh-flow{-ms-flex-positive:1;-ms-flex-direction:column;flex-direction:column;flex-grow:1;min-height:100%;overflow-y:auto}.gh-flow,.gh-flow-head{display:-ms-flexbox;display:flex}.gh-flow-head{-ms-flex-negative:0;-ms-flex-pack:justify;flex-shrink:0;justify-content:space-between;padding-bottom:20px;padding-top:4vh}.gh-flow-content-wrap{-ms-flex-positive:1;-ms-flex-negative:0;-ms-flex-pack:center;flex-grow:1;flex-shrink:0;justify-content:center;margin:0 5%;padding-bottom:8vh}.gh-flow-back,.gh-flow-content-wrap{-ms-flex-align:center;align-items:center;display:-ms-flexbox;display:flex}.gh-flow-back{border:1px solid transparent;border-radius:4px;color:#7d878a;font-weight:100;left:0;margin:0 0 0 3%;padding:2px 9px 2px 5px;position:absolute;top:0;transition:all .3s ease}.gh-flow-back svg{height:12px;line-height:14px;margin-right:4px}.gh-flow-back svg path{stroke:#7d878a;stroke-width:1.2px}.gh-flow-back:hover{border:1px solid #dae1e3}.gh-flow-back-plain{-ms-flex-align:center;align-items:center;color:#7d878a;display:-ms-flexbox;display:flex;font-weight:300;left:0;margin:0 0 0 3%;padding:2px 9px 2px 5px;position:absolute;text-decoration:none;top:0;transition:all .3s ease}.gh-flow-back-plain svg{height:12px;line-height:14px;margin-right:4px}.gh-flow-back-plain svg path{stroke:#7d878a;stroke-width:1.2px}.gh-flow-back-plain:hover{color:#15212a}.gh-flow-nav{-ms-flex:1;flex:1;position:relative}.gh-flow-content{color:#738a94;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;font-size:1.9rem;font-weight:100;line-height:1.5em;max-width:700px;text-align:center;width:100%}.gh-flow-content .gh-input-icon input{padding-left:35px}.gh-flow-content-unsubscribe{font-weight:300}@media (max-width:500px){.gh-flow-head-unsubscribe{padding-top:2.8vh}.gh-flow-content{font-size:4vw}.gh-flow-content-unsubscribe{font-size:1.8rem;line-height:1.6em}}.gh-flow-content header{margin:0 auto;max-width:520px}.gh-flow-content h1{font-size:4.2rem;font-weight:100}@media (max-width:600px){.gh-flow-content h1{font-size:7vw}}.gh-flow-content .gh-btn{display:block;margin:20px auto 0;max-width:400px}.gh-flow-content .form-group{margin-bottom:2.5rem}.gh-flow-content input{border:1px solid #dae1e3;font-size:1.6rem;font-weight:100;line-height:1.4em;padding:10px}.gh-flow-em{font-weight:500}.gh-signin{background:#f8fbfd;border:1px solid #dae1e3;border-radius:5px;margin:30px auto;max-width:400px;padding:40px;position:relative;text-align:left;width:100%}.gh-signin .form-group{margin-bottom:1.5rem}.gh-signin .gh-btn{margin:0}.error-content{flex-grow:1;justify-content:center;padding:8vw;user-select:text}.error-content,.error-details{align-items:center;display:flex}.error-details{margin-bottom:4rem}.error-ghost{height:115px;margin:15px}@media (max-width:630px){.error-ghost{display:none}}.error-code{color:#c5d2d9;font-size:10vw;font-weight:600;letter-spacing:-.4vw;line-height:.9em;margin:0}.error-description{border:none;color:#54666d;font-size:2.3rem;font-weight:300;line-height:1.3em;margin:0;padding:0}.error-message{align-items:center;display:flex;flex-direction:column;margin:15px}.error-message a{font-size:1.4rem;line-height:1;margin:8px 0}.error-link{background-color:transparent;color:#5ba4e5;text-decoration:none;transition:background .3s,color .3s}.error-stack{background-color:hsla(0,0%,100%,.3);margin:1rem auto;max-width:800px;padding:2rem}.error-stack-list{list-style-type:none;margin:0;padding:0}.error-stack-list li{display:block}.error-stack-list li:before{color:#bbb;content:"\21AA";display:inline-block;font-size:1.2rem;margin-right:.5rem}.error-stack-function{font-weight:700}
@@ -216,7 +216,7 @@ class ImageSize {
216
216
  // get the storage readable filePath
217
217
  filePath = this.storageUtils.getLocalFileStoragePath(imagePath);
218
218
 
219
- return this.storage.getStorage()
219
+ return this.storage.getStorage('images')
220
220
  .read({path: filePath})
221
221
  .then((buf) => {
222
222
  debug('Image fetched (storage):', filePath);
@@ -267,7 +267,7 @@ class ImageSize {
267
267
  }
268
268
 
269
269
  const originalImagePath = path.join(dir, `${imageName}_o${imageNumber || ''}${ext}`);
270
- const originalImageExists = await this.storage.getStorage().exists(originalImagePath);
270
+ const originalImageExists = await this.storage.getStorage('images').exists(originalImagePath);
271
271
 
272
272
  return this.getImageSizeFromStoragePath(originalImageExists ? originalImagePath : imagePath);
273
273
  }
@@ -3,7 +3,6 @@ const errors = require('@tryghost/errors');
3
3
  const logging = require('@tryghost/logging');
4
4
  const config = require('../../shared/config');
5
5
  const storage = require('../adapters/storage');
6
- const imageTransform = require('@tryghost/image-transform');
7
6
 
8
7
  let cardFactory;
9
8
  let cards;
@@ -34,11 +33,13 @@ module.exports = {
34
33
  siteUrl: config.get('url'),
35
34
  imageOptimization: config.get('imageOptimization'),
36
35
  canTransformImage(storagePath) {
36
+ const imageTransform = require('@tryghost/image-transform');
37
37
  const {ext} = path.parse(storagePath);
38
38
 
39
+ // NOTE: the "saveRaw" check is smelly
39
40
  return imageTransform.canTransformFiles()
40
41
  && imageTransform.canTransformFileExtension(ext)
41
- && typeof storage.getStorage().saveRaw === 'function';
42
+ && typeof storage.getStorage('images').saveRaw === 'function';
42
43
  }
43
44
  });
44
45
 
@@ -3,13 +3,16 @@ const ghostBookshelf = require('./base');
3
3
 
4
4
  const candidates = [];
5
5
 
6
- _.each(ghostBookshelf.registry.models, (model) => {
7
- candidates.push([model, model.prototype.tableName.replace(/s$/, '')]);
8
- });
9
-
10
6
  const Action = ghostBookshelf.Model.extend({
11
7
  tableName: 'actions',
12
8
 
9
+ initialize: function initialize() {
10
+ _.each(ghostBookshelf.registry.models, (model) => {
11
+ candidates.push([model, model.prototype.tableName.replace(/s$/, '')]);
12
+ });
13
+ this.constructor.__super__.initialize.apply(this, arguments);
14
+ },
15
+
13
16
  actor() {
14
17
  return this.morphTo('actor', ['actor_type', 'actor_id'], ...candidates);
15
18
  },
@@ -19,15 +19,24 @@ module.exports = function (Bookshelf) {
19
19
  const originalInsertSync = parentSync.insert;
20
20
  const self = this;
21
21
 
22
- // deep clone attrs to avoid modifying underlying model attributes by reference
23
22
  parentSync.update = function update(attrs) {
24
- attrs = self.formatOnWrite(_.cloneDeep(attrs));
25
- return originalUpdateSync.apply(this, [attrs]);
23
+ self._isWriting = true;
24
+
25
+ const originalPromise = originalUpdateSync.apply(this, [attrs]);
26
+
27
+ return originalPromise.finally(function () {
28
+ self._isWriting = false;
29
+ });
26
30
  };
27
31
 
28
- parentSync.insert = function insert(attrs) {
29
- attrs = self.formatOnWrite(_.cloneDeep(attrs));
30
- return originalInsertSync.apply(this, [attrs]);
32
+ parentSync.insert = function insert() {
33
+ self._isWriting = true;
34
+
35
+ const originalPromise = originalInsertSync.apply(this);
36
+
37
+ return originalPromise.finally(function () {
38
+ self._isWriting = false;
39
+ });
31
40
  };
32
41
 
33
42
  return parentSync;
@@ -42,6 +51,10 @@ module.exports = function (Bookshelf) {
42
51
 
43
52
  // format date before writing to DB, bools work
44
53
  format: function format(attrs) {
54
+ if (this._isWriting) {
55
+ attrs = this.formatOnWrite(attrs);
56
+ }
57
+
45
58
  return this.fixDatesWhenSave(attrs);
46
59
  },
47
60
 
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  const _ = require('lodash');
6
+ const glob = require('glob');
6
7
 
7
8
  // enable event listeners
8
9
  require('./base/listeners');
@@ -12,55 +13,12 @@ require('./base/listeners');
12
13
  */
13
14
  exports = module.exports;
14
15
 
15
- const models = [
16
- 'permission',
17
- 'post',
18
- 'role',
19
- 'settings',
20
- 'custom-theme-setting',
21
- 'session',
22
- 'tag',
23
- 'tag-public',
24
- 'user',
25
- 'author',
26
- 'invite',
27
- 'webhook',
28
- 'integration',
29
- 'api-key',
30
- 'mobiledoc-revision',
31
- 'member',
32
- 'offer',
33
- 'offer-redemption',
34
- 'product',
35
- 'benefit',
36
- 'stripe-product',
37
- 'stripe-price',
38
- 'member-subscribe-event',
39
- 'member-paid-subscription-event',
40
- 'member-login-event',
41
- 'member-email-change-event',
42
- 'member-payment-event',
43
- 'member-status-event',
44
- 'member-product-event',
45
- 'member-analytic-event',
46
- 'posts-meta',
47
- 'member-stripe-customer',
48
- 'stripe-customer-subscription',
49
- 'email',
50
- 'email-batch',
51
- 'email-recipient',
52
- 'label',
53
- 'single-use-token',
54
- 'snippet',
55
- // Action model MUST be loaded last as it loops through all of the registered models
56
- // Please do not append items to this array.
57
- 'action'
58
- ];
59
-
60
16
  function init() {
61
17
  exports.Base = require('./base');
62
18
 
63
- models.forEach(function (name) {
19
+ let modelsFiles = glob.sync('!(index).js', {cwd: __dirname});
20
+ modelsFiles.forEach((model) => {
21
+ const name = model.replace(/.js$/, '');
64
22
  _.extend(exports, require('./' + name));
65
23
  });
66
24
  }
@@ -94,6 +94,11 @@ const Member = ghostBookshelf.Model.extend({
94
94
  });
95
95
  },
96
96
 
97
+ offerRedemptions() {
98
+ return this.hasMany('OfferRedemption', 'member_id', 'id')
99
+ .query('orderBy', 'created_at', 'DESC');
100
+ },
101
+
97
102
  labels: function labels() {
98
103
  return this.belongsToMany('Label', 'members_labels', 'member_id', 'label_id')
99
104
  .withPivot('sort_order')
@@ -8,7 +8,6 @@ const limitService = require('../services/limits');
8
8
  const tpl = require('@tryghost/tpl');
9
9
  const errors = require('@tryghost/errors');
10
10
  const security = require('@tryghost/security');
11
- const {gravatar} = require('../lib/image');
12
11
  const {pipeline} = require('@tryghost/promise');
13
12
  const validatePassword = require('../lib/validate-password');
14
13
  const permissions = require('../services/permissions');
@@ -191,6 +190,8 @@ User = ghostBookshelf.Model.extend({
191
190
  // If the user's email is set & has changed & we are not importing
192
191
  if (self.hasChanged('email') && self.get('email') && !options.importing) {
193
192
  tasks.gravatar = (function lookUpGravatar() {
193
+ const {gravatar} = require('../lib/image');
194
+
194
195
  return gravatar.lookup({
195
196
  email: self.get('email')
196
197
  }).then(function (response) {
@@ -5,13 +5,17 @@
5
5
  */
6
6
  process.env.BLUEBIRD_DEBUG = 0;
7
7
 
8
+ const luxon = require('luxon');
8
9
  const moment = require('moment-timezone');
9
10
 
10
11
  /**
11
12
  * force UTC
12
- * - you can require moment or moment-timezone, both is configured to UTC
13
+ * - old way: you can require moment or moment-timezone
14
+ * - new way: you should use Luxon - work is in progress to switch from moment.
15
+ *
13
16
  * - you are allowed to use new Date() to instantiate datetime values for models, because they are transformed into UTC in the model layer
14
17
  * - be careful when not working with models, every value from the native JS Date is local TZ
15
- * - be careful when you work with date operations, therefor always wrap a date into moment
18
+ * - be careful when you work with date operations, therefore always wrap a date with our timezone library
16
19
  */
20
+ luxon.Settings.defaultZone = 'UTC';
17
21
  moment.tz.setDefault('UTC');
@@ -1,4 +1,5 @@
1
1
  /**
2
+ * Maps configuration from the config file to a unified adapter config in following form:
2
3
  * {
3
4
  * [adapterType]: {
4
5
  * active: [adapterName],
@@ -1,5 +1,6 @@
1
1
  const AdapterManager = require('@tryghost/adapter-manager');
2
2
  const getAdapterServiceConfig = require('./config');
3
+ const resolveAdapterOptions = require('./options-resolver');
3
4
  const config = require('../../../shared/config');
4
5
 
5
6
  const adapterManager = new AdapterManager({
@@ -16,13 +17,16 @@ adapterManager.registerAdapter('scheduling', require('../../adapters/scheduling/
16
17
  adapterManager.registerAdapter('sso', require('../../adapters/sso/Base'));
17
18
 
18
19
  module.exports = {
19
- getAdapter(adapterType) {
20
+ /**
21
+ *
22
+ * @param {String} name - one of 'storage', 'scheduling', 'sso' etc. Or can contain a "resource" extension like "storage:image"
23
+ * @returns {Object} instance of an adapter
24
+ */
25
+ getAdapter(name) {
20
26
  const adapterServiceConfig = getAdapterServiceConfig(config);
21
27
 
22
- const adapterSettings = adapterServiceConfig[adapterType];
23
- const activeAdapter = adapterSettings.active;
24
- const activeAdapterConfig = adapterSettings[activeAdapter];
28
+ const {adapterType, adapterName, adapterConfig} = resolveAdapterOptions(name, adapterServiceConfig);
25
29
 
26
- return adapterManager.getAdapter(adapterType, activeAdapter, activeAdapterConfig);
30
+ return adapterManager.getAdapter(adapterType, adapterName, adapterConfig);
27
31
  }
28
32
  };