ghost 4.18.0 → 4.20.1

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 (237) 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 +17 -12
  6. package/core/bridge.js +9 -1
  7. package/core/built/assets/{chunk.3.b80d3e1e6b8556aaff3c.js → chunk.3.777d43e2ce954ba8b2f5.js} +25 -25
  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-88d647a008a5b1dd678a89ae1e55c038.js → ghost.min-26e427944e719b616b8dc7fbb3bbd2f9.js} +709 -422
  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/icons/paintbrush.svg +10 -1
  14. package/core/built/assets/icons/post.svg +3 -1
  15. package/core/built/assets/img/footer-marketplace-bg-572b6c6486a7e26316954d599eaa9f30.png +0 -0
  16. package/core/built/assets/img/marketing/offers-1-f2e1b653c4d5bb90eea9d7a2862530f9.jpg +0 -0
  17. package/core/built/assets/img/marketing/offers-2-28a225d34cc39d133748431536961d00.jpg +0 -0
  18. package/core/built/assets/img/marketing/offers-3-2094c91ab21a16c37fbe6ec16c140160.jpg +0 -0
  19. package/core/built/assets/img/themes/Alto-f4db5af43ca9771c7ac1f754de3ddf2f.png +0 -0
  20. package/core/built/assets/img/themes/Bulletin-57d45b992ff0e26e0acdce7ed4cccd67.png +0 -0
  21. package/core/built/assets/img/themes/Casper-c7e784d7188cc5d7f097d9b6c97b0263.jpg +0 -0
  22. package/core/built/assets/img/themes/Dawn-be81aa8c8caae8fcfb5d5fbec823fdcc.png +0 -0
  23. package/core/built/assets/img/themes/Digest-d3467ac22a290e1ad3a543014758286e.png +0 -0
  24. package/core/built/assets/img/themes/Dope-6f8e0bbc199ce4af9a60859e9e6a74ad.png +0 -0
  25. package/core/built/assets/img/themes/Ease-9c279ea6cec3c0f1823f81c9dd24b116.png +0 -0
  26. package/core/built/assets/img/themes/Edge-0258906309e11fd075a1d9880aa09b20.png +0 -0
  27. package/core/built/assets/img/themes/Edition-d8f508e93bc24bdf2716ae6f8b3d44f8.png +0 -0
  28. package/core/built/assets/img/themes/Editorial-a25a4a34c04dedd858bd5e05ef388b1c.jpg +0 -0
  29. package/core/built/assets/img/themes/Journal-accf0031bbae0919900a049061e65a04.png +0 -0
  30. package/core/built/assets/img/themes/London-3f07efcee9e5bfb9a33827064eb77e70.jpg +0 -0
  31. package/core/built/assets/img/themes/Massively-06edf00108429f7fb8e65f190fba34fe.jpg +0 -0
  32. package/core/built/assets/img/themes/Ruby-11a53c62015612f4b3aca8f503121225.png +0 -0
  33. package/core/built/assets/img/themes/Wave-86e8044c2d76cb57a9030e4c24ac9520.png +0 -0
  34. package/core/built/assets/simplemde/{simplemde-232f69d126310434489071a1891e6d8b.js → simplemde-3ffc0ec9e9fecf29b9a499db678c9e65.js} +14 -14
  35. package/core/built/assets/{vendor.min-7dc7cf9c92175ebfb9cea95c120ee8a7.js → vendor.min-af502ac4142871500fc424f6a5a254ec.js} +2206 -1859
  36. package/core/frontend/apps/amp/lib/router.js +1 -1
  37. package/core/frontend/helpers/match.js +17 -23
  38. package/core/frontend/meta/author-url.js +1 -1
  39. package/core/frontend/meta/url.js +1 -1
  40. package/core/{server → frontend}/public/favicon.ico +0 -0
  41. package/core/{server → frontend}/public/ghost.css +0 -0
  42. package/core/{server → frontend}/public/ghost.min.css +0 -0
  43. package/core/{server → frontend}/public/robots.txt +0 -0
  44. package/core/{server → frontend}/public/sitemap.xsl +0 -0
  45. package/core/frontend/services/proxy.js +1 -1
  46. package/core/frontend/services/rendering.js +1 -1
  47. package/core/frontend/services/routing/CollectionRouter.js +3 -49
  48. package/core/frontend/services/routing/ParentRouter.js +1 -4
  49. package/core/frontend/services/routing/StaticPagesRouter.js +3 -5
  50. package/core/frontend/services/routing/StaticRoutesRouter.js +4 -6
  51. package/core/frontend/services/routing/TaxonomyRouter.js +4 -5
  52. package/core/frontend/services/routing/controllers/collection.js +2 -2
  53. package/core/frontend/services/routing/controllers/email-post.js +2 -2
  54. package/core/frontend/services/routing/controllers/entry.js +2 -2
  55. package/core/frontend/services/routing/controllers/preview.js +2 -2
  56. package/core/frontend/services/routing/index.js +6 -12
  57. package/core/frontend/services/routing/registry.js +13 -0
  58. package/core/frontend/services/routing/router-manager.js +185 -0
  59. package/core/frontend/services/rss/generate-feed.js +2 -2
  60. package/core/frontend/services/theme-engine/i18n/i18n.js +267 -28
  61. package/core/frontend/services/theme-engine/i18n/index.js +1 -1
  62. package/core/frontend/services/theme-engine/i18n/theme-i18n.js +73 -0
  63. package/core/frontend/web/index.js +1 -0
  64. package/core/{server/web/site → frontend/web}/middleware/handle-image-sizes.js +4 -4
  65. package/core/{server/web/site → frontend/web}/middleware/index.js +0 -0
  66. package/core/{server/web/site → frontend/web}/middleware/redirect-ghost-to-admin.js +3 -3
  67. package/core/{server/web/site → frontend/web}/middleware/serve-favicon.js +6 -6
  68. package/core/{server/web/site → frontend/web}/middleware/serve-public-file.js +2 -2
  69. package/core/{server/web/site → frontend/web}/middleware/static-theme.js +3 -3
  70. package/core/frontend/web/routes.js +13 -0
  71. package/core/{server/web/site/app.js → frontend/web/site.js} +12 -16
  72. package/core/server/adapters/storage/LocalFileStorage.js +35 -39
  73. package/core/server/adapters/storage/index.js +12 -2
  74. package/core/server/api/canary/custom-theme-settings.js +2 -2
  75. package/core/server/api/canary/images.js +1 -1
  76. package/core/server/api/canary/oembed.js +2 -2
  77. package/core/server/api/canary/offers.js +29 -1
  78. package/core/server/api/canary/posts-public.js +6 -2
  79. package/core/server/api/canary/products.js +6 -2
  80. package/core/server/api/canary/tags-public.js +6 -2
  81. package/core/server/api/canary/users.js +9 -4
  82. package/core/server/api/canary/utils/serializers/output/custom-theme-settings.js +2 -2
  83. package/core/server/api/canary/utils/serializers/output/notifications.js +1 -0
  84. package/core/server/api/canary/utils/serializers/output/settings.js +2 -3
  85. package/core/server/api/canary/utils/serializers/output/utils/url.js +1 -1
  86. package/core/server/api/canary/utils/validators/input/oembed.js +4 -1
  87. package/core/server/api/canary/utils/validators/input/passwordreset.js +8 -3
  88. package/core/server/api/canary/utils/validators/input/settings.js +5 -4
  89. package/core/server/api/canary/utils/validators/input/setup.js +6 -2
  90. package/core/server/api/canary/utils/validators/input/users.js +6 -2
  91. package/core/server/api/canary/utils/validators/input/webhooks.js +8 -3
  92. package/core/server/api/v2/images.js +1 -1
  93. package/core/server/api/v2/utils/serializers/output/authentication.js +9 -4
  94. package/core/server/api/v2/utils/serializers/output/notifications.js +1 -0
  95. package/core/server/api/v2/utils/serializers/output/users.js +5 -3
  96. package/core/server/api/v2/utils/serializers/output/utils/url.js +1 -1
  97. package/core/server/api/v2/utils/validators/input/images.js +11 -6
  98. package/core/server/api/v2/utils/validators/input/invitations.js +14 -6
  99. package/core/server/api/v2/utils/validators/input/invites.js +6 -2
  100. package/core/server/api/v2/utils/validators/input/oembed.js +6 -2
  101. package/core/server/api/v2/utils/validators/input/passwordreset.js +8 -3
  102. package/core/server/api/v2/utils/validators/input/settings.js +10 -4
  103. package/core/server/api/v2/utils/validators/input/setup.js +6 -2
  104. package/core/server/api/v2/utils/validators/input/users.js +5 -2
  105. package/core/server/api/v3/authentication.js +6 -2
  106. package/core/server/api/v3/authors-public.js +6 -2
  107. package/core/server/api/v3/email.js +9 -4
  108. package/core/server/api/v3/images.js +1 -1
  109. package/core/server/api/v3/integrations.js +7 -3
  110. package/core/server/api/v3/invites.js +6 -3
  111. package/core/server/api/v3/labels.js +10 -5
  112. package/core/server/api/v3/memberSigninUrls.js +5 -2
  113. package/core/server/api/v3/oembed.js +2 -2
  114. package/core/server/api/v3/pages-public.js +5 -2
  115. package/core/server/api/v3/pages.js +6 -3
  116. package/core/server/api/v3/posts-public.js +5 -3
  117. package/core/server/api/v3/posts.js +7 -3
  118. package/core/server/api/v3/preview.js +5 -3
  119. package/core/server/api/v3/session.js +7 -3
  120. package/core/server/api/v3/settings.js +8 -3
  121. package/core/server/api/v3/slugs.js +5 -4
  122. package/core/server/api/v3/utils/serializers/output/authentication.js +10 -4
  123. package/core/server/api/v3/utils/serializers/output/notifications.js +1 -0
  124. package/core/server/api/v3/utils/serializers/output/settings.js +2 -3
  125. package/core/server/api/v3/utils/serializers/output/users.js +6 -2
  126. package/core/server/api/v3/utils/serializers/output/utils/url.js +1 -1
  127. package/core/server/api/v3/utils/validators/input/images.js +12 -7
  128. package/core/server/api/v3/utils/validators/input/invitations.js +14 -6
  129. package/core/server/api/v3/utils/validators/input/invites.js +6 -2
  130. package/core/server/api/v3/utils/validators/input/oembed.js +6 -2
  131. package/core/server/api/v3/utils/validators/input/passwordreset.js +8 -3
  132. package/core/server/api/v3/utils/validators/input/settings.js +5 -4
  133. package/core/server/api/v3/utils/validators/input/setup.js +6 -2
  134. package/core/server/api/v3/utils/validators/input/users.js +6 -2
  135. package/core/server/api/v3/utils/validators/input/webhooks.js +8 -3
  136. package/core/server/data/exporter/table-lists.js +2 -1
  137. package/core/server/data/importer/handlers/image.js +1 -1
  138. package/core/server/data/importer/importers/image.js +1 -1
  139. package/core/server/data/migrations/init/1-create-tables.js +7 -8
  140. package/core/server/data/migrations/init/2-create-fixtures.js +8 -8
  141. package/core/server/data/migrations/versions/4.19/01-add-active-column-to-offers.js +7 -0
  142. package/core/server/data/migrations/versions/4.19/02-add-offer-redemptions-table.js +8 -0
  143. package/core/server/data/migrations/versions/4.20/01-remove-offer-redemptions-table.js +19 -0
  144. package/core/server/data/migrations/versions/4.20/02-remove-offers-table.js +30 -0
  145. package/core/server/data/migrations/versions/4.20/03-add-offers-table.js +21 -0
  146. package/core/server/data/migrations/versions/4.20/04-add-offer-redemptions-table.js +9 -0
  147. package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +41 -0
  148. package/core/server/data/schema/fixtures/utils.js +150 -143
  149. package/core/server/data/schema/schema.js +15 -3
  150. package/core/server/frontend/ghost.min.css +1 -0
  151. package/core/server/lib/image/blog-icon.js +10 -10
  152. package/core/server/lib/image/image-size.js +5 -5
  153. package/core/server/lib/image/image-utils.js +4 -4
  154. package/core/server/lib/image/index.js +1 -2
  155. package/core/server/lib/mobiledoc.js +3 -2
  156. package/core/server/models/action.js +7 -4
  157. package/core/server/models/base/plugins/overrides.js +19 -6
  158. package/core/server/models/custom-theme-setting.js +56 -1
  159. package/core/server/models/index.js +4 -45
  160. package/core/server/models/member.js +5 -0
  161. package/core/server/models/offer-redemption.js +10 -0
  162. package/core/server/models/user.js +2 -1
  163. package/core/server/overrides.js +6 -2
  164. package/core/server/run-update-check.js +0 -3
  165. package/core/server/services/adapter-manager/config.js +1 -0
  166. package/core/server/services/adapter-manager/index.js +9 -5
  167. package/core/server/services/adapter-manager/options-resolver.js +18 -0
  168. package/core/server/services/bulk-email/bulk-email-processor.js +6 -2
  169. package/core/server/services/bulk-email/mailgun.js +1 -1
  170. package/core/server/services/custom-theme-settings.js +10 -4
  171. package/core/server/services/invites/index.js +0 -2
  172. package/core/server/services/invites/invites.js +5 -5
  173. package/core/server/services/mail/GhostMailer.js +18 -10
  174. package/core/server/services/mega/mega.js +3 -3
  175. package/core/server/services/mega/post-email-serializer.js +2 -2
  176. package/core/server/services/members/api.js +3 -4
  177. package/core/server/services/members/emails/signin.js +1 -1
  178. package/core/server/services/members/emails/signup.js +1 -1
  179. package/core/server/services/members/emails/subscribe.js +1 -1
  180. package/core/server/services/members/middleware.js +10 -0
  181. package/core/server/services/members/service.js +2 -1
  182. package/core/server/services/notifications/index.js +1 -1
  183. package/core/server/services/notifications/notifications.js +40 -35
  184. package/core/server/services/oembed.js +4 -9
  185. package/core/server/services/offers/service.js +16 -6
  186. package/core/server/services/permissions/public.js +6 -2
  187. package/core/server/services/route-settings/route-settings.js +1 -1
  188. package/core/server/services/settings/index.js +3 -1
  189. package/core/server/services/settings/settings-bread-service.js +42 -20
  190. package/core/server/services/slack.js +1 -1
  191. package/core/server/services/themes/activate.js +2 -2
  192. package/core/server/services/themes/activation-bridge.js +6 -6
  193. package/core/server/services/themes/storage.js +1 -1
  194. package/core/{frontend → server}/services/url/Queue.js +0 -0
  195. package/core/{frontend → server}/services/url/Resource.js +0 -0
  196. package/core/{frontend → server}/services/url/Resources.js +2 -2
  197. package/core/{frontend → server}/services/url/UrlGenerator.js +14 -14
  198. package/core/{frontend → server}/services/url/UrlService.js +12 -15
  199. package/core/{frontend → server}/services/url/Urls.js +1 -1
  200. package/core/{frontend → server}/services/url/configs/canary.js +0 -0
  201. package/core/{frontend → server}/services/url/configs/v2.js +0 -0
  202. package/core/{frontend → server}/services/url/configs/v3.js +0 -0
  203. package/core/{frontend → server}/services/url/configs/v4.js +0 -0
  204. package/core/{frontend → server}/services/url/index.js +0 -0
  205. package/core/server/services/xmlrpc.js +1 -1
  206. package/core/server/update-check.js +3 -3
  207. package/core/server/web/admin/controller.js +11 -0
  208. package/core/server/web/admin/views/default-prod.html +4 -4
  209. package/core/server/web/admin/views/default.html +4 -4
  210. package/core/server/web/api/app.js +8 -9
  211. package/core/server/web/members/app.js +1 -0
  212. package/core/server/web/oauth/app.js +4 -2
  213. package/core/server/web/parent/backend.js +3 -3
  214. package/core/server/web/parent/frontend.js +2 -2
  215. package/core/server/web/shared/middlewares/api/spam-prevention.js +0 -2
  216. package/core/server/web/shared/middlewares/custom-redirects.js +0 -8
  217. package/core/server/web/shared/middlewares/maintenance.js +1 -1
  218. package/core/server/web/well-known.js +10 -10
  219. package/core/shared/config/defaults.json +2 -2
  220. package/core/shared/config/overrides.json +1 -1
  221. package/core/shared/express.js +10 -0
  222. package/core/shared/html-to-plaintext.js +2 -2
  223. package/core/shared/labs.js +14 -6
  224. package/loggingrc.js +10 -0
  225. package/package.json +60 -57
  226. package/yarn.lock +1317 -914
  227. package/core/built/assets/ghost-dark-13627f10941a7dbb2b12e1d41dc51c34.css +0 -1
  228. package/core/built/assets/ghost.min-d9cbfb4eb2db8915fcd2bf2416218616.css +0 -1
  229. package/core/built/assets/img/themes/London-68501c8ab797de7f2851cf9ea0a28e26.jpg +0 -0
  230. package/core/frontend/services/routing/bootstrap.js +0 -114
  231. package/core/server/public/404-ghost.png +0 -0
  232. package/core/server/public/404-ghost@2x.png +0 -0
  233. package/core/server/web/site/index.js +0 -1
  234. package/core/server/web/site/routes.js +0 -9
  235. package/core/shared/i18n/i18n.js +0 -312
  236. package/core/shared/i18n/index.js +0 -6
  237. package/core/shared/i18n/translations/en.json +0 -675
@@ -4,12 +4,12 @@ const Gravatar = require('./gravatar');
4
4
  const ImageSize = require('./image-size');
5
5
 
6
6
  class ImageUtils {
7
- constructor({config, logging, tpl, urlUtils, settingsCache, storageUtils, storage, validator, request}) {
8
- this.blogIcon = new BlogIcon({config, tpl, urlUtils, settingsCache, storageUtils});
9
- this.imageSize = new ImageSize({config, tpl, storage, storageUtils, validator, urlUtils, request});
7
+ constructor({config, logging, urlUtils, settingsCache, storageUtils, storage, validator, request}) {
8
+ this.blogIcon = new BlogIcon({config, urlUtils, settingsCache, storageUtils});
9
+ this.imageSize = new ImageSize({config, storage, storageUtils, validator, urlUtils, request});
10
10
  this.cachedImageSizeFromUrl = new CachedImageSizeFromUrl({logging, imageSize: this.imageSize});
11
11
  this.gravatar = new Gravatar({config, request});
12
12
  }
13
13
  }
14
14
 
15
- module.exports = ImageUtils;
15
+ module.exports = ImageUtils;
@@ -5,8 +5,7 @@ const storageUtils = require('../../adapters/storage/utils');
5
5
  const validator = require('@tryghost/validator');
6
6
  const config = require('../../../shared/config');
7
7
  const logging = require('@tryghost/logging');
8
- const tpl = require('@tryghost/tpl');
9
8
  const settingsCache = require('../../../shared/settings-cache');
10
9
  const ImageUtils = require('./image-utils');
11
10
 
12
- module.exports = new ImageUtils({config, logging, tpl, urlUtils, settingsCache, storageUtils, storage, validator, request});
11
+ module.exports = new ImageUtils({config, logging, urlUtils, settingsCache, storageUtils, storage, validator, request});
@@ -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
 
@@ -1,7 +1,62 @@
1
+ const _ = require('lodash');
1
2
  const ghostBookshelf = require('./base');
3
+ const urlUtils = require('../../shared/url-utils');
2
4
 
3
5
  const CustomThemeSetting = ghostBookshelf.Model.extend({
4
- tableName: 'custom_theme_settings'
6
+ tableName: 'custom_theme_settings',
7
+
8
+ parse() {
9
+ const attrs = ghostBookshelf.Model.prototype.parse.apply(this, arguments);
10
+ const settingType = attrs.type;
11
+
12
+ // transform "0" to false for boolean type
13
+ if (settingType === 'boolean' && (attrs.value === '0' || attrs.value === '1')) {
14
+ attrs.value = !!+attrs.value;
15
+ }
16
+
17
+ // transform "false" to false for boolean type
18
+ if (settingType === 'boolean' && (attrs.value === 'false' || attrs.value === 'true')) {
19
+ attrs.value = JSON.parse(attrs.value);
20
+ }
21
+
22
+ // transform URLs to absolute for image settings
23
+ if (settingType === 'image' && attrs.value) {
24
+ attrs.value = urlUtils.transformReadyToAbsolute(attrs.value);
25
+ }
26
+
27
+ return attrs;
28
+ },
29
+
30
+ format() {
31
+ const attrs = ghostBookshelf.Model.prototype.format.apply(this, arguments);
32
+ const settingType = attrs.type;
33
+
34
+ if (settingType === 'boolean') {
35
+ // CASE: Ensure we won't forward strings, otherwise model events or model interactions can fail
36
+ if (attrs.value === '0' || attrs.value === '1') {
37
+ attrs.value = !!+attrs.value;
38
+ }
39
+
40
+ // CASE: Ensure we won't forward strings, otherwise model events or model interactions can fail
41
+ if (attrs.value === 'false' || attrs.value === 'true') {
42
+ attrs.value = JSON.parse(attrs.value);
43
+ }
44
+
45
+ if (_.isBoolean(attrs.value)) {
46
+ attrs.value = attrs.value.toString();
47
+ }
48
+ }
49
+
50
+ return attrs;
51
+ },
52
+
53
+ formatOnWrite(attrs) {
54
+ if (attrs.type === 'image' && attrs.value) {
55
+ attrs.value = urlUtils.toTransformReady(attrs.value);
56
+ }
57
+
58
+ return attrs;
59
+ }
5
60
  });
6
61
 
7
62
  module.exports = {
@@ -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,54 +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
- 'product',
34
- 'benefit',
35
- 'stripe-product',
36
- 'stripe-price',
37
- 'member-subscribe-event',
38
- 'member-paid-subscription-event',
39
- 'member-login-event',
40
- 'member-email-change-event',
41
- 'member-payment-event',
42
- 'member-status-event',
43
- 'member-product-event',
44
- 'member-analytic-event',
45
- 'posts-meta',
46
- 'member-stripe-customer',
47
- 'stripe-customer-subscription',
48
- 'email',
49
- 'email-batch',
50
- 'email-recipient',
51
- 'label',
52
- 'single-use-token',
53
- 'snippet',
54
- // Action model MUST be loaded last as it loops through all of the registered models
55
- // Please do not append items to this array.
56
- 'action'
57
- ];
58
-
59
16
  function init() {
60
17
  exports.Base = require('./base');
61
18
 
62
- models.forEach(function (name) {
19
+ let modelsFiles = glob.sync('!(index).js', {cwd: __dirname});
20
+ modelsFiles.forEach((model) => {
21
+ const name = model.replace(/.js$/, '');
63
22
  _.extend(exports, require('./' + name));
64
23
  });
65
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')
@@ -0,0 +1,10 @@
1
+ const ghostBookshelf = require('./base');
2
+
3
+ const OfferRedemption = ghostBookshelf.Model.extend({
4
+ tableName: 'offer_redemptions'
5
+ });
6
+
7
+ module.exports = {
8
+ OfferRedemption: ghostBookshelf.model('OfferRedemption', OfferRedemption)
9
+ };
10
+
@@ -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');
@@ -52,9 +52,6 @@ if (parentPort) {
52
52
 
53
53
  const settings = require('./services/settings');
54
54
  await settings.init();
55
-
56
- const i18n = require('../shared/i18n');
57
- i18n.init();
58
55
  // Finished INIT
59
56
 
60
57
  await updateCheck({logging});
@@ -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
  };
@@ -0,0 +1,18 @@
1
+ module.exports = function resolveAdapterOptions(name, adapterServiceConfig) {
2
+ const [adapterType, feature] = name.split(':');
3
+ const adapterSettings = adapterServiceConfig[adapterType];
4
+
5
+ let adapterName;
6
+ let adapterConfig;
7
+
8
+ // CASE: load resource-specific adapter when there is an adapter feature name specified as well as custom feature config
9
+ if (feature && adapterSettings[feature] && adapterSettings[adapterSettings[feature]]) {
10
+ adapterName = adapterSettings[feature];
11
+ adapterConfig = adapterSettings[adapterName];
12
+ } else {
13
+ adapterName = adapterSettings.active;
14
+ adapterConfig = adapterSettings[adapterName];
15
+ }
16
+
17
+ return {adapterType, adapterName, adapterConfig};
18
+ };
@@ -2,7 +2,7 @@ const _ = require('lodash');
2
2
  const Promise = require('bluebird');
3
3
  const moment = require('moment-timezone');
4
4
  const errors = require('@tryghost/errors');
5
- const i18n = require('../../../shared/i18n');
5
+ const tpl = require('@tryghost/tpl');
6
6
  const logging = require('@tryghost/logging');
7
7
  const models = require('../../models');
8
8
  const mailgunProvider = require('./mailgun');
@@ -10,6 +10,10 @@ const sentry = require('../../../shared/sentry');
10
10
  const debug = require('@tryghost/debug')('mega');
11
11
  const postEmailSerializer = require('../mega/post-email-serializer');
12
12
 
13
+ const messages = {
14
+ error: 'The email service was unable to send an email batch.'
15
+ };
16
+
13
17
  const BATCH_SIZE = mailgunProvider.BATCH_SIZE;
14
18
 
15
19
  /**
@@ -239,7 +243,7 @@ module.exports = {
239
243
  // REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
240
244
  let ghostError = new errors.EmailError({
241
245
  err: error,
242
- context: i18n.t('errors.services.mega.requestFailed.error'),
246
+ context: tpl(messages.error),
243
247
  code: 'BULK_EMAIL_SEND_FAILED'
244
248
  });
245
249
 
@@ -1,6 +1,5 @@
1
1
  const _ = require('lodash');
2
2
  const {URL} = require('url');
3
- const mailgun = require('mailgun-js');
4
3
  const logging = require('@tryghost/logging');
5
4
  const configService = require('../../../shared/config');
6
5
  const settingsCache = require('../../../shared/settings-cache');
@@ -8,6 +7,7 @@ const settingsCache = require('../../../shared/settings-cache');
8
7
  const BATCH_SIZE = 1000;
9
8
 
10
9
  function createMailgun(config) {
10
+ const mailgun = require('mailgun-js');
11
11
  const baseUrl = new URL(config.baseUrl);
12
12
 
13
13
  return mailgun({
@@ -2,7 +2,13 @@ const {Service: CustomThemeSettingsService} = require('@tryghost/custom-theme-se
2
2
  const customThemeSettingsCache = require('../../shared/custom-theme-settings-cache');
3
3
  const models = require('../models');
4
4
 
5
- module.exports = new CustomThemeSettingsService({
6
- model: models.CustomThemeSetting,
7
- cache: customThemeSettingsCache
8
- });
5
+ class CustomThemeSettingsServiceWrapper {
6
+ init() {
7
+ this.api = new CustomThemeSettingsService({
8
+ model: models.CustomThemeSetting,
9
+ cache: customThemeSettingsCache
10
+ });
11
+ }
12
+ }
13
+
14
+ module.exports = new CustomThemeSettingsServiceWrapper();
@@ -1,5 +1,4 @@
1
1
  const settingsCache = require('../../../shared/settings-cache');
2
- const tpl = require('@tryghost/tpl');
3
2
  const mailService = require('../../services/mail');
4
3
  const logging = require('@tryghost/logging');
5
4
  const urlUtils = require('../../../shared/url-utils');
@@ -7,7 +6,6 @@ const Invites = require('./invites');
7
6
 
8
7
  module.exports = new Invites({
9
8
  settingsCache,
10
- tpl,
11
9
  logging,
12
10
  mailService,
13
11
  urlUtils
@@ -1,4 +1,5 @@
1
1
  const security = require('@tryghost/security');
2
+ const tpl = require('@tryghost/tpl');
2
3
 
3
4
  const messages = {
4
5
  invitedByName: '{invitedByName} has invited you to join {blogName}',
@@ -9,9 +10,8 @@ const messages = {
9
10
  };
10
11
 
11
12
  class Invites {
12
- constructor({settingsCache, tpl, logging, mailService, urlUtils}) {
13
+ constructor({settingsCache, logging, mailService, urlUtils}) {
13
14
  this.settingsCache = settingsCache;
14
- this.tpl = tpl;
15
15
  this.logging = logging;
16
16
  this.mailService = mailService;
17
17
  this.urlUtils = urlUtils;
@@ -52,7 +52,7 @@ class Invites {
52
52
  mail: [{
53
53
  message: {
54
54
  to: invite.get('email'),
55
- subject: this.tpl(messages.invitedByName, {
55
+ subject: tpl(messages.invitedByName, {
56
56
  invitedByName: emailData.invitedByName,
57
57
  blogName: emailData.blogName
58
58
  }),
@@ -75,10 +75,10 @@ class Invites {
75
75
  })
76
76
  .catch((err) => {
77
77
  if (err && err.errorType === 'EmailError') {
78
- const errorMessage = this.tpl(messages.errorSendingEmail.error, {
78
+ const errorMessage = tpl(messages.errorSendingEmail.error, {
79
79
  message: err.message
80
80
  });
81
- const helpText = this.tpl(messages.errorSendingEmail.help);
81
+ const helpText = tpl(messages.errorSendingEmail.help);
82
82
  err.message = `${errorMessage} ${helpText}`;
83
83
  this.logging.warn(err.message);
84
84
  }
@@ -4,9 +4,17 @@ const _ = require('lodash');
4
4
  const validator = require('@tryghost/validator');
5
5
  const config = require('../../../shared/config');
6
6
  const errors = require('@tryghost/errors');
7
- const i18n = require('../../../shared/i18n');
7
+ const tpl = require('@tryghost/tpl');
8
8
  const settingsCache = require('../../../shared/settings-cache');
9
9
  const urlUtils = require('../../../shared/url-utils');
10
+ const messages = {
11
+ title: 'Ghost at {domain}',
12
+ checkEmailConfigInstructions: 'Please see {url} for instructions on configuring email.',
13
+ failedSendingEmailError: 'Failed to send email.',
14
+ incompleteMessageDataError: 'Incomplete message data.',
15
+ reason: ' Reason: {reason}.',
16
+ messageSent: 'Message sent. Double check inbox and spam folder!'
17
+ };
10
18
 
11
19
  function getDomain() {
12
20
  const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
@@ -25,7 +33,7 @@ function getFromAddress(requestedFromAddress) {
25
33
 
26
34
  // If we do have a from address, and it's just an email
27
35
  if (validator.isEmail(address, {require_tld: false})) {
28
- const defaultSiteTitle = settingsCache.get('title') ? settingsCache.get('title').replace(/"/g, '\\"') : i18n.t('common.mail.title', {domain: getDomain()});
36
+ const defaultSiteTitle = settingsCache.get('title') ? settingsCache.get('title').replace(/"/g, '\\"') : tpl(messages.title, {domain: getDomain()});
29
37
  return `"${defaultSiteTitle}" <${address}>`;
30
38
  }
31
39
 
@@ -51,8 +59,8 @@ function createMessage(message) {
51
59
  }
52
60
 
53
61
  function createMailError({message, err, ignoreDefaultMessage} = {message: ''}) {
54
- const helpMessage = i18n.t('errors.api.authentication.checkEmailConfigInstructions', {url: 'https://ghost.org/docs/config/#mail'});
55
- const defaultErrorMessage = i18n.t('errors.mail.failedSendingEmail.error');
62
+ const helpMessage = tpl(messages.checkEmailConfigInstructions, {url: 'https://ghost.org/docs/config/#mail'});
63
+ const defaultErrorMessage = tpl(messages.failedSendingEmailError);
56
64
 
57
65
  const fullErrorMessage = defaultErrorMessage + message;
58
66
  let statusCode = (err && err.name === 'RecipientError') ? 400 : 500;
@@ -95,7 +103,7 @@ module.exports = class GhostMailer {
95
103
  async send(message) {
96
104
  if (!(message && message.subject && message.html && message.to)) {
97
105
  throw createMailError({
98
- message: i18n.t('errors.mail.incompleteMessageData.error'),
106
+ message: tpl(messages.incompleteMessageDataError),
99
107
  ignoreDefaultMessage: true
100
108
  });
101
109
  }
@@ -117,7 +125,7 @@ module.exports = class GhostMailer {
117
125
  return response;
118
126
  } catch (err) {
119
127
  throw createMailError({
120
- message: i18n.t('errors.mail.reason', {reason: err.message || err}),
128
+ message: tpl(messages.reason, {reason: err.message || err}),
121
129
  err
122
130
  });
123
131
  }
@@ -125,21 +133,21 @@ module.exports = class GhostMailer {
125
133
 
126
134
  handleDirectTransportResponse(response) {
127
135
  if (!response) {
128
- return i18n.t('notices.mail.messageSent');
136
+ return tpl(messages.messageSent);
129
137
  }
130
138
 
131
139
  if (response.pending.length > 0) {
132
140
  throw createMailError({
133
- message: i18n.t('errors.mail.reason', {reason: 'Email has been temporarily rejected'})
141
+ message: tpl(messages.reason, {reason: 'Email has been temporarily rejected'})
134
142
  });
135
143
  }
136
144
 
137
145
  if (response.errors.length > 0) {
138
146
  throw createMailError({
139
- message: i18n.t('errors.mail.reason', {reason: response.errors[0].message})
147
+ message: tpl(messages.reason, {reason: response.errors[0].message})
140
148
  });
141
149
  }
142
150
 
143
- return i18n.t('notices.mail.messageSent');
151
+ return tpl(messages.messageSent);
144
152
  }
145
153
  };
@@ -6,7 +6,6 @@ const url = require('url');
6
6
  const moment = require('moment');
7
7
  const ObjectID = require('bson-objectid');
8
8
  const errors = require('@tryghost/errors');
9
- const i18n = require('../../../shared/i18n');
10
9
  const logging = require('@tryghost/logging');
11
10
  const settingsCache = require('../../../shared/settings-cache');
12
11
  const membersService = require('../members');
@@ -25,7 +24,8 @@ const messages = {
25
24
  invalidSegment: 'Invalid segment value. Use one of the valid:"status:free" or "status:-free" values.',
26
25
  unexpectedFilterError: 'Unexpected {property} value "{value}", expected an NQL equivalent',
27
26
  noneFilterError: 'Cannot send email to "none" {property}',
28
- emailSendingDisabled: `Email sending is temporarily disabled because your account is currently in review. You should have an email about this from us already, but you can also reach us any time at support@ghost.org`
27
+ emailSendingDisabled: `Email sending is temporarily disabled because your account is currently in review. You should have an email about this from us already, but you can also reach us any time at support@ghost.org`,
28
+ sendEmailRequestFailed: 'The email service was unable to send an email batch.'
29
29
  };
30
30
 
31
31
  const getFromAddress = () => {
@@ -354,7 +354,7 @@ async function sendEmailJob({emailModel, options}) {
354
354
 
355
355
  throw new errors.GhostError({
356
356
  err: error,
357
- context: i18n.t('errors.services.mega.requestFailed.error')
357
+ context: tpl(messages.sendEmailRequestFailed)
358
358
  });
359
359
  }
360
360
  }
@@ -1,5 +1,4 @@
1
1
  const _ = require('lodash');
2
- const juice = require('juice');
3
2
  const template = require('./template');
4
3
  const settingsCache = require('../../../shared/settings-cache');
5
4
  const urlUtils = require('../../../shared/url-utils');
@@ -20,6 +19,7 @@ const ALLOWED_REPLACEMENTS = ['first_name'];
20
19
  const formatHtmlForEmail = function formatHtmlForEmail(html) {
21
20
  const juiceOptions = {inlinePseudoElements: true};
22
21
 
22
+ const juice = require('juice');
23
23
  let juicedHtml = juice(html, juiceOptions);
24
24
 
25
25
  // convert juiced HTML to a DOM-like interface for further manipulation
@@ -227,7 +227,7 @@ const serialize = async (postModel, options = {isBrowserPreview: false, apiVersi
227
227
  const momentDate = post.published_at ? moment(post.published_at) : moment();
228
228
  post.published_at = momentDate.tz(timezone).format('DD MMM YYYY');
229
229
 
230
- post.authors = post.authors && post.authors.map(author => author.name).join(',');
230
+ post.authors = post.authors && post.authors.map(author => author.name).join(', ');
231
231
  if (post.posts_meta) {
232
232
  post.email_subject = post.posts_meta.email_subject;
233
233
  }