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.
- package/.eslintrc.js +9 -8
- package/Gruntfile.js +1 -1
- package/PRIVACY.md +3 -0
- package/content/adapters/README.md +2 -2
- package/core/boot.js +17 -12
- package/core/bridge.js +9 -1
- package/core/built/assets/{chunk.3.b80d3e1e6b8556aaff3c.js → chunk.3.777d43e2ce954ba8b2f5.js} +25 -25
- package/core/built/assets/codemirror/{codemirror-21a09582262987037db73b152fb35f7c.js → codemirror-d25c379b87ec8b33d54ac7149bc0b6ae.js} +14 -14
- package/core/built/assets/ghost-dark-20e2892d4f30d0d1183c9ac725ea37d0.css +1 -0
- package/core/built/assets/{ghost.min-88d647a008a5b1dd678a89ae1e55c038.js → ghost.min-26e427944e719b616b8dc7fbb3bbd2f9.js} +709 -422
- package/core/built/assets/ghost.min-57e46fd3b1145ecf2cbd185a13611f3b.css +1 -0
- package/core/built/assets/icons/arrow-left-small.svg +0 -4
- package/core/built/assets/icons/paintbrush.svg +10 -1
- package/core/built/assets/icons/post.svg +3 -1
- package/core/built/assets/img/footer-marketplace-bg-572b6c6486a7e26316954d599eaa9f30.png +0 -0
- package/core/built/assets/img/marketing/offers-1-f2e1b653c4d5bb90eea9d7a2862530f9.jpg +0 -0
- package/core/built/assets/img/marketing/offers-2-28a225d34cc39d133748431536961d00.jpg +0 -0
- package/core/built/assets/img/marketing/offers-3-2094c91ab21a16c37fbe6ec16c140160.jpg +0 -0
- package/core/built/assets/img/themes/Alto-f4db5af43ca9771c7ac1f754de3ddf2f.png +0 -0
- package/core/built/assets/img/themes/Bulletin-57d45b992ff0e26e0acdce7ed4cccd67.png +0 -0
- package/core/built/assets/img/themes/Casper-c7e784d7188cc5d7f097d9b6c97b0263.jpg +0 -0
- package/core/built/assets/img/themes/Dawn-be81aa8c8caae8fcfb5d5fbec823fdcc.png +0 -0
- package/core/built/assets/img/themes/Digest-d3467ac22a290e1ad3a543014758286e.png +0 -0
- package/core/built/assets/img/themes/Dope-6f8e0bbc199ce4af9a60859e9e6a74ad.png +0 -0
- package/core/built/assets/img/themes/Ease-9c279ea6cec3c0f1823f81c9dd24b116.png +0 -0
- package/core/built/assets/img/themes/Edge-0258906309e11fd075a1d9880aa09b20.png +0 -0
- package/core/built/assets/img/themes/Edition-d8f508e93bc24bdf2716ae6f8b3d44f8.png +0 -0
- package/core/built/assets/img/themes/Editorial-a25a4a34c04dedd858bd5e05ef388b1c.jpg +0 -0
- package/core/built/assets/img/themes/Journal-accf0031bbae0919900a049061e65a04.png +0 -0
- package/core/built/assets/img/themes/London-3f07efcee9e5bfb9a33827064eb77e70.jpg +0 -0
- package/core/built/assets/img/themes/Massively-06edf00108429f7fb8e65f190fba34fe.jpg +0 -0
- package/core/built/assets/img/themes/Ruby-11a53c62015612f4b3aca8f503121225.png +0 -0
- package/core/built/assets/img/themes/Wave-86e8044c2d76cb57a9030e4c24ac9520.png +0 -0
- package/core/built/assets/simplemde/{simplemde-232f69d126310434489071a1891e6d8b.js → simplemde-3ffc0ec9e9fecf29b9a499db678c9e65.js} +14 -14
- package/core/built/assets/{vendor.min-7dc7cf9c92175ebfb9cea95c120ee8a7.js → vendor.min-af502ac4142871500fc424f6a5a254ec.js} +2206 -1859
- package/core/frontend/apps/amp/lib/router.js +1 -1
- package/core/frontend/helpers/match.js +17 -23
- package/core/frontend/meta/author-url.js +1 -1
- package/core/frontend/meta/url.js +1 -1
- package/core/{server → frontend}/public/favicon.ico +0 -0
- package/core/{server → frontend}/public/ghost.css +0 -0
- package/core/{server → frontend}/public/ghost.min.css +0 -0
- package/core/{server → frontend}/public/robots.txt +0 -0
- package/core/{server → frontend}/public/sitemap.xsl +0 -0
- package/core/frontend/services/proxy.js +1 -1
- package/core/frontend/services/rendering.js +1 -1
- package/core/frontend/services/routing/CollectionRouter.js +3 -49
- package/core/frontend/services/routing/ParentRouter.js +1 -4
- package/core/frontend/services/routing/StaticPagesRouter.js +3 -5
- package/core/frontend/services/routing/StaticRoutesRouter.js +4 -6
- package/core/frontend/services/routing/TaxonomyRouter.js +4 -5
- package/core/frontend/services/routing/controllers/collection.js +2 -2
- package/core/frontend/services/routing/controllers/email-post.js +2 -2
- package/core/frontend/services/routing/controllers/entry.js +2 -2
- package/core/frontend/services/routing/controllers/preview.js +2 -2
- package/core/frontend/services/routing/index.js +6 -12
- package/core/frontend/services/routing/registry.js +13 -0
- package/core/frontend/services/routing/router-manager.js +185 -0
- package/core/frontend/services/rss/generate-feed.js +2 -2
- package/core/frontend/services/theme-engine/i18n/i18n.js +267 -28
- package/core/frontend/services/theme-engine/i18n/index.js +1 -1
- package/core/frontend/services/theme-engine/i18n/theme-i18n.js +73 -0
- package/core/frontend/web/index.js +1 -0
- package/core/{server/web/site → frontend/web}/middleware/handle-image-sizes.js +4 -4
- package/core/{server/web/site → frontend/web}/middleware/index.js +0 -0
- package/core/{server/web/site → frontend/web}/middleware/redirect-ghost-to-admin.js +3 -3
- package/core/{server/web/site → frontend/web}/middleware/serve-favicon.js +6 -6
- package/core/{server/web/site → frontend/web}/middleware/serve-public-file.js +2 -2
- package/core/{server/web/site → frontend/web}/middleware/static-theme.js +3 -3
- package/core/frontend/web/routes.js +13 -0
- package/core/{server/web/site/app.js → frontend/web/site.js} +12 -16
- package/core/server/adapters/storage/LocalFileStorage.js +35 -39
- package/core/server/adapters/storage/index.js +12 -2
- package/core/server/api/canary/custom-theme-settings.js +2 -2
- package/core/server/api/canary/images.js +1 -1
- package/core/server/api/canary/oembed.js +2 -2
- package/core/server/api/canary/offers.js +29 -1
- package/core/server/api/canary/posts-public.js +6 -2
- package/core/server/api/canary/products.js +6 -2
- package/core/server/api/canary/tags-public.js +6 -2
- package/core/server/api/canary/users.js +9 -4
- package/core/server/api/canary/utils/serializers/output/custom-theme-settings.js +2 -2
- package/core/server/api/canary/utils/serializers/output/notifications.js +1 -0
- package/core/server/api/canary/utils/serializers/output/settings.js +2 -3
- package/core/server/api/canary/utils/serializers/output/utils/url.js +1 -1
- package/core/server/api/canary/utils/validators/input/oembed.js +4 -1
- package/core/server/api/canary/utils/validators/input/passwordreset.js +8 -3
- package/core/server/api/canary/utils/validators/input/settings.js +5 -4
- package/core/server/api/canary/utils/validators/input/setup.js +6 -2
- package/core/server/api/canary/utils/validators/input/users.js +6 -2
- package/core/server/api/canary/utils/validators/input/webhooks.js +8 -3
- package/core/server/api/v2/images.js +1 -1
- package/core/server/api/v2/utils/serializers/output/authentication.js +9 -4
- package/core/server/api/v2/utils/serializers/output/notifications.js +1 -0
- package/core/server/api/v2/utils/serializers/output/users.js +5 -3
- package/core/server/api/v2/utils/serializers/output/utils/url.js +1 -1
- package/core/server/api/v2/utils/validators/input/images.js +11 -6
- package/core/server/api/v2/utils/validators/input/invitations.js +14 -6
- package/core/server/api/v2/utils/validators/input/invites.js +6 -2
- package/core/server/api/v2/utils/validators/input/oembed.js +6 -2
- package/core/server/api/v2/utils/validators/input/passwordreset.js +8 -3
- package/core/server/api/v2/utils/validators/input/settings.js +10 -4
- package/core/server/api/v2/utils/validators/input/setup.js +6 -2
- package/core/server/api/v2/utils/validators/input/users.js +5 -2
- package/core/server/api/v3/authentication.js +6 -2
- package/core/server/api/v3/authors-public.js +6 -2
- package/core/server/api/v3/email.js +9 -4
- package/core/server/api/v3/images.js +1 -1
- package/core/server/api/v3/integrations.js +7 -3
- package/core/server/api/v3/invites.js +6 -3
- package/core/server/api/v3/labels.js +10 -5
- package/core/server/api/v3/memberSigninUrls.js +5 -2
- package/core/server/api/v3/oembed.js +2 -2
- package/core/server/api/v3/pages-public.js +5 -2
- package/core/server/api/v3/pages.js +6 -3
- package/core/server/api/v3/posts-public.js +5 -3
- package/core/server/api/v3/posts.js +7 -3
- package/core/server/api/v3/preview.js +5 -3
- package/core/server/api/v3/session.js +7 -3
- package/core/server/api/v3/settings.js +8 -3
- package/core/server/api/v3/slugs.js +5 -4
- package/core/server/api/v3/utils/serializers/output/authentication.js +10 -4
- package/core/server/api/v3/utils/serializers/output/notifications.js +1 -0
- package/core/server/api/v3/utils/serializers/output/settings.js +2 -3
- package/core/server/api/v3/utils/serializers/output/users.js +6 -2
- package/core/server/api/v3/utils/serializers/output/utils/url.js +1 -1
- package/core/server/api/v3/utils/validators/input/images.js +12 -7
- package/core/server/api/v3/utils/validators/input/invitations.js +14 -6
- package/core/server/api/v3/utils/validators/input/invites.js +6 -2
- package/core/server/api/v3/utils/validators/input/oembed.js +6 -2
- package/core/server/api/v3/utils/validators/input/passwordreset.js +8 -3
- package/core/server/api/v3/utils/validators/input/settings.js +5 -4
- package/core/server/api/v3/utils/validators/input/setup.js +6 -2
- package/core/server/api/v3/utils/validators/input/users.js +6 -2
- package/core/server/api/v3/utils/validators/input/webhooks.js +8 -3
- package/core/server/data/exporter/table-lists.js +2 -1
- package/core/server/data/importer/handlers/image.js +1 -1
- package/core/server/data/importer/importers/image.js +1 -1
- package/core/server/data/migrations/init/1-create-tables.js +7 -8
- package/core/server/data/migrations/init/2-create-fixtures.js +8 -8
- package/core/server/data/migrations/versions/4.19/01-add-active-column-to-offers.js +7 -0
- package/core/server/data/migrations/versions/4.19/02-add-offer-redemptions-table.js +8 -0
- package/core/server/data/migrations/versions/4.20/01-remove-offer-redemptions-table.js +19 -0
- package/core/server/data/migrations/versions/4.20/02-remove-offers-table.js +30 -0
- package/core/server/data/migrations/versions/4.20/03-add-offers-table.js +21 -0
- package/core/server/data/migrations/versions/4.20/04-add-offer-redemptions-table.js +9 -0
- package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +41 -0
- package/core/server/data/schema/fixtures/utils.js +150 -143
- package/core/server/data/schema/schema.js +15 -3
- package/core/server/frontend/ghost.min.css +1 -0
- package/core/server/lib/image/blog-icon.js +10 -10
- package/core/server/lib/image/image-size.js +5 -5
- package/core/server/lib/image/image-utils.js +4 -4
- package/core/server/lib/image/index.js +1 -2
- package/core/server/lib/mobiledoc.js +3 -2
- package/core/server/models/action.js +7 -4
- package/core/server/models/base/plugins/overrides.js +19 -6
- package/core/server/models/custom-theme-setting.js +56 -1
- package/core/server/models/index.js +4 -45
- package/core/server/models/member.js +5 -0
- package/core/server/models/offer-redemption.js +10 -0
- package/core/server/models/user.js +2 -1
- package/core/server/overrides.js +6 -2
- package/core/server/run-update-check.js +0 -3
- package/core/server/services/adapter-manager/config.js +1 -0
- package/core/server/services/adapter-manager/index.js +9 -5
- package/core/server/services/adapter-manager/options-resolver.js +18 -0
- package/core/server/services/bulk-email/bulk-email-processor.js +6 -2
- package/core/server/services/bulk-email/mailgun.js +1 -1
- package/core/server/services/custom-theme-settings.js +10 -4
- package/core/server/services/invites/index.js +0 -2
- package/core/server/services/invites/invites.js +5 -5
- package/core/server/services/mail/GhostMailer.js +18 -10
- package/core/server/services/mega/mega.js +3 -3
- package/core/server/services/mega/post-email-serializer.js +2 -2
- package/core/server/services/members/api.js +3 -4
- package/core/server/services/members/emails/signin.js +1 -1
- package/core/server/services/members/emails/signup.js +1 -1
- package/core/server/services/members/emails/subscribe.js +1 -1
- package/core/server/services/members/middleware.js +10 -0
- package/core/server/services/members/service.js +2 -1
- package/core/server/services/notifications/index.js +1 -1
- package/core/server/services/notifications/notifications.js +40 -35
- package/core/server/services/oembed.js +4 -9
- package/core/server/services/offers/service.js +16 -6
- package/core/server/services/permissions/public.js +6 -2
- package/core/server/services/route-settings/route-settings.js +1 -1
- package/core/server/services/settings/index.js +3 -1
- package/core/server/services/settings/settings-bread-service.js +42 -20
- package/core/server/services/slack.js +1 -1
- package/core/server/services/themes/activate.js +2 -2
- package/core/server/services/themes/activation-bridge.js +6 -6
- package/core/server/services/themes/storage.js +1 -1
- package/core/{frontend → server}/services/url/Queue.js +0 -0
- package/core/{frontend → server}/services/url/Resource.js +0 -0
- package/core/{frontend → server}/services/url/Resources.js +2 -2
- package/core/{frontend → server}/services/url/UrlGenerator.js +14 -14
- package/core/{frontend → server}/services/url/UrlService.js +12 -15
- package/core/{frontend → server}/services/url/Urls.js +1 -1
- package/core/{frontend → server}/services/url/configs/canary.js +0 -0
- package/core/{frontend → server}/services/url/configs/v2.js +0 -0
- package/core/{frontend → server}/services/url/configs/v3.js +0 -0
- package/core/{frontend → server}/services/url/configs/v4.js +0 -0
- package/core/{frontend → server}/services/url/index.js +0 -0
- package/core/server/services/xmlrpc.js +1 -1
- package/core/server/update-check.js +3 -3
- package/core/server/web/admin/controller.js +11 -0
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/app.js +8 -9
- package/core/server/web/members/app.js +1 -0
- package/core/server/web/oauth/app.js +4 -2
- package/core/server/web/parent/backend.js +3 -3
- package/core/server/web/parent/frontend.js +2 -2
- package/core/server/web/shared/middlewares/api/spam-prevention.js +0 -2
- package/core/server/web/shared/middlewares/custom-redirects.js +0 -8
- package/core/server/web/shared/middlewares/maintenance.js +1 -1
- package/core/server/web/well-known.js +10 -10
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/config/overrides.json +1 -1
- package/core/shared/express.js +10 -0
- package/core/shared/html-to-plaintext.js +2 -2
- package/core/shared/labs.js +14 -6
- package/loggingrc.js +10 -0
- package/package.json +60 -57
- package/yarn.lock +1317 -914
- package/core/built/assets/ghost-dark-13627f10941a7dbb2b12e1d41dc51c34.css +0 -1
- package/core/built/assets/ghost.min-d9cbfb4eb2db8915fcd2bf2416218616.css +0 -1
- package/core/built/assets/img/themes/London-68501c8ab797de7f2851cf9ea0a28e26.jpg +0 -0
- package/core/frontend/services/routing/bootstrap.js +0 -114
- package/core/server/public/404-ghost.png +0 -0
- package/core/server/public/404-ghost@2x.png +0 -0
- package/core/server/web/site/index.js +0 -1
- package/core/server/web/site/routes.js +0 -9
- package/core/shared/i18n/i18n.js +0 -312
- package/core/shared/i18n/index.js +0 -6
- 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,
|
|
8
|
-
this.blogIcon = new BlogIcon({config,
|
|
9
|
-
this.imageSize = new ImageSize({config,
|
|
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,
|
|
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
|
-
|
|
25
|
-
|
|
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(
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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')
|
|
@@ -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) {
|
package/core/server/overrides.js
CHANGED
|
@@ -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
|
|
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,
|
|
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,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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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:
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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,
|
|
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:
|
|
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 =
|
|
78
|
+
const errorMessage = tpl(messages.errorSendingEmail.error, {
|
|
79
79
|
message: err.message
|
|
80
80
|
});
|
|
81
|
-
const helpText =
|
|
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
|
|
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, '\\"') :
|
|
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 =
|
|
55
|
-
const defaultErrorMessage =
|
|
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:
|
|
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:
|
|
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
|
|
136
|
+
return tpl(messages.messageSent);
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
if (response.pending.length > 0) {
|
|
132
140
|
throw createMailError({
|
|
133
|
-
message:
|
|
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:
|
|
147
|
+
message: tpl(messages.reason, {reason: response.errors[0].message})
|
|
140
148
|
});
|
|
141
149
|
}
|
|
142
150
|
|
|
143
|
-
return
|
|
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:
|
|
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
|
}
|