ghost 5.8.1 → 5.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tryghost-adapter-manager-0.0.0.tgz +0 -0
- package/components/tryghost-api-framework-0.0.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-0.0.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-0.0.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-0.0.0.tgz +0 -0
- package/components/tryghost-job-manager-0.0.0.tgz +0 -0
- package/components/tryghost-mailgun-client-0.0.0.tgz +0 -0
- package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
- package/components/tryghost-members-api-0.0.0.tgz +0 -0
- package/components/tryghost-members-importer-0.0.0.tgz +0 -0
- package/components/tryghost-members-offers-0.0.0.tgz +0 -0
- package/components/tryghost-members-payments-0.0.0.tgz +0 -0
- package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
- package/components/tryghost-minifier-0.0.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-0.0.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-0.0.0.tgz +0 -0
- package/components/tryghost-package-json-0.0.0.tgz +0 -0
- package/components/tryghost-session-service-0.0.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
- package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
- package/content/themes/casper/README.md +1 -1
- package/content/themes/casper/assets/built/portal.min.js +3 -0
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +119 -37
- package/content/themes/casper/package.json +3 -3
- package/content/themes/casper/partials/post-card.hbs +3 -1
- package/content/themes/casper/post.hbs +7 -7
- package/content/themes/casper/yarn.lock +69 -68
- package/core/boot.js +2 -0
- package/core/built/admin/assets/{chunk.143.904e017ad397bb681e9d.js → chunk.143.e35fdb482bc822313f0c.js} +5 -5
- package/core/built/admin/assets/{chunk.178.9541bdb92bded29cd60d.js → chunk.178.1d381d687652f2597fe2.js} +4 -4
- package/core/built/admin/assets/{chunk.351.cbc224ca65c14ef5322d.js → chunk.351.73f27952f867334a8228.js} +3 -3
- package/core/built/admin/assets/{chunk.351.cbc224ca65c14ef5322d.js.LICENSE.txt → chunk.351.73f27952f867334a8228.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/{ghost-b469423d0fbe5e40af17b560f7e3cead.css → ghost-686c383caa6a3469cefb939ab10e21b6.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-bcb6f4517a2dfe23a0a280632bfca00c.css → ghost-dark-6814c399ff5b3d9c8efe2d92bc7ec779.css} +1 -1
- package/core/built/admin/assets/{ghost-a66a04418efe85083a3adca0fb16bb52.js → ghost-eca1a709a74b1af277e48aad4e16c9db.js} +94 -92
- package/core/built/admin/assets/{vendor-46baf13852f545c6c89756c8e0ccbff2.js → vendor-516c9e43b4aeb92079dc1ab92c9ce492.js} +3 -3
- package/core/built/admin/index.html +6 -6
- package/core/frontend/helpers/comment_count.js +7 -15
- package/core/frontend/helpers/comments.js +4 -16
- package/core/frontend/helpers/ghost_head.js +1 -1
- package/core/frontend/public/ghost.min.css +1 -1
- package/core/frontend/src/comment-counts/js/comment-counts.js +12 -1
- package/core/server/api/endpoints/comments-members.js +23 -1
- package/core/server/api/endpoints/index.js +52 -52
- package/core/server/api/endpoints/utils/serializers/input/comments.js +18 -0
- package/core/server/api/endpoints/utils/serializers/input/db.js +1 -1
- package/core/server/api/endpoints/utils/serializers/input/index.js +4 -0
- package/core/server/api/endpoints/utils/serializers/input/integrations.js +2 -2
- package/core/server/api/endpoints/utils/serializers/input/tiers.js +1 -1
- package/core/server/api/{shared → endpoints/utils}/serializers/input/utils/settings-filter-type-group-mapper.js +0 -0
- package/core/server/api/{shared → endpoints/utils}/serializers/input/utils/settings-key-group-mapper.js +0 -0
- package/core/server/api/{shared → endpoints/utils}/serializers/input/utils/settings-key-type-mapper.js +0 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +12 -12
- package/core/server/api/endpoints/utils/serializers/output/tiers.js +1 -1
- package/core/server/api/index.js +0 -2
- package/core/server/data/db/connection.js +0 -4
- package/core/server/data/importer/importers/data/settings.js +2 -2
- package/core/server/data/migrations/versions/5.9/2022-08-09-08-32-added-new-integration-type.js +24 -0
- package/core/server/data/schema/clients/mysql.js +0 -15
- package/core/server/data/schema/commands.js +0 -9
- package/core/server/data/schema/fixtures/fixtures.json +1 -1
- package/core/server/data/schema/schema.js +3 -3
- package/core/server/models/base/plugins/user-type.js +1 -9
- package/core/server/models/comment.js +96 -15
- package/core/server/models/label.js +14 -0
- package/core/server/models/newsletter.js +21 -0
- package/core/server/models/stripe-customer-subscription.js +6 -0
- package/core/server/models/tag.js +20 -0
- package/core/server/models/user.js +20 -0
- package/core/server/run-update-check.js +1 -1
- package/core/server/services/auth/api-key/admin.js +1 -1
- package/core/server/services/auth/api-key/content.js +1 -1
- package/core/server/services/bulk-email/bulk-email-processor.js +18 -11
- package/core/server/services/bulk-email/index.js +1 -17
- package/core/server/services/comments/controller.js +9 -0
- package/core/server/services/comments/email-templates/new-comment-reply.hbs +2 -2
- package/core/server/services/comments/email-templates/new-comment.hbs +2 -2
- package/core/server/services/comments/email-templates/report.hbs +2 -2
- package/core/server/services/comments/service.js +16 -3
- package/core/server/services/comments/stats.js +2 -0
- package/core/server/services/email-analytics/jobs/fetch-latest.js +1 -1
- package/core/server/services/mega/post-email-serializer.js +2 -2
- package/core/server/services/permissions/can-this.js +154 -161
- package/core/server/services/permissions/parse-context.js +1 -8
- package/core/server/services/webhooks/serialize.js +3 -3
- package/core/server/web/api/endpoints/admin/routes.js +1 -1
- package/core/server/web/api/endpoints/content/routes.js +1 -1
- package/core/server/web/api/testmode/jobs/graceful-job.js +1 -1
- package/core/server/web/comments/routes.js +2 -1
- package/core/server/web/shared/middleware/index.js +1 -1
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/labs.js +0 -1
- package/package.json +31 -29
- package/yarn.lock +330 -276
- package/core/server/api/README.md +0 -130
- package/core/server/api/shared/frame.js +0 -95
- package/core/server/api/shared/headers.js +0 -152
- package/core/server/api/shared/http.js +0 -127
- package/core/server/api/shared/index.js +0 -25
- package/core/server/api/shared/pipeline.js +0 -259
- package/core/server/api/shared/serializers/handle.js +0 -140
- package/core/server/api/shared/serializers/index.js +0 -13
- package/core/server/api/shared/serializers/input/all.js +0 -41
- package/core/server/api/shared/serializers/input/index.js +0 -5
- package/core/server/api/shared/serializers/output/index.js +0 -1
- package/core/server/api/shared/utils/index.js +0 -5
- package/core/server/api/shared/utils/options.js +0 -23
- package/core/server/api/shared/validators/handle.js +0 -68
- package/core/server/api/shared/validators/index.js +0 -9
- package/core/server/api/shared/validators/input/all.js +0 -213
- package/core/server/api/shared/validators/input/index.js +0 -5
- package/core/server/services/bulk-email/mailgun.js +0 -122
- package/core/server/web/shared/middleware/cache-control.js +0 -43
|
@@ -3,7 +3,7 @@ const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output
|
|
|
3
3
|
|
|
4
4
|
const allowedIncludes = ['monthly_price', 'yearly_price'];
|
|
5
5
|
const localUtils = require('../../index');
|
|
6
|
-
const utils = require('
|
|
6
|
+
const {utils} = require('@tryghost/api-framework');
|
|
7
7
|
const labs = require('../../../../../../shared/labs');
|
|
8
8
|
|
|
9
9
|
module.exports = {
|
package/core/server/api/index.js
CHANGED
|
@@ -31,10 +31,6 @@ function configure(dbConfig) {
|
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
// Force bthreads to use child_process backend until a worker_thread-compatible version of sqlite3 is published
|
|
35
|
-
// https://github.com/mapbox/node-sqlite3/issues/1386
|
|
36
|
-
process.env.BTHREADS_BACKEND = 'child_process';
|
|
37
|
-
|
|
38
34
|
// In the default SQLite test config we set the path to /tmp/ghost-test.db,
|
|
39
35
|
// but this won't work on Windows, so we need to replace the /tmp bit with
|
|
40
36
|
// the Windows temp folder
|
|
@@ -5,8 +5,8 @@ const _ = require('lodash');
|
|
|
5
5
|
const BaseImporter = require('./base');
|
|
6
6
|
const models = require('../../../../models');
|
|
7
7
|
const defaultSettings = require('../../../schema').defaultSettings;
|
|
8
|
-
const keyGroupMapper = require('../../../../api/
|
|
9
|
-
const keyTypeMapper = require('../../../../api/
|
|
8
|
+
const keyGroupMapper = require('../../../../api/endpoints/utils/serializers/input/utils/settings-key-group-mapper');
|
|
9
|
+
const keyTypeMapper = require('../../../../api/endpoints/utils/serializers/input/utils/settings-key-type-mapper');
|
|
10
10
|
const {WRITABLE_KEYS_ALLOWLIST} = require('../../../../../shared/labs');
|
|
11
11
|
|
|
12
12
|
const labsDefaults = JSON.parse(defaultSettings.labs.labs.defaultValue);
|
package/core/server/data/migrations/versions/5.9/2022-08-09-08-32-added-new-integration-type.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
3
|
+
|
|
4
|
+
module.exports = createTransactionalMigration(
|
|
5
|
+
async function up(knex) {
|
|
6
|
+
logging.info('Changing Ghost Explore Integration to type "core"');
|
|
7
|
+
await knex('integrations')
|
|
8
|
+
.where({
|
|
9
|
+
name: 'Ghost Explore',
|
|
10
|
+
slug: 'ghost-explore'
|
|
11
|
+
})
|
|
12
|
+
.update('type', 'core');
|
|
13
|
+
},
|
|
14
|
+
async function down(knex) {
|
|
15
|
+
logging.info('Changing Ghost Explore Integration to type "builtin"');
|
|
16
|
+
|
|
17
|
+
await knex('integrations')
|
|
18
|
+
.where({
|
|
19
|
+
name: 'Ghost Explore',
|
|
20
|
+
slug: 'ghost-explore'
|
|
21
|
+
})
|
|
22
|
+
.update('type', 'builtin');
|
|
23
|
+
}
|
|
24
|
+
);
|
|
@@ -27,22 +27,7 @@ const getColumns = function getColumns(table, transaction) {
|
|
|
27
27
|
});
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
// This function changes the type of posts.html and posts.markdown columns to mediumtext. Due to
|
|
31
|
-
// a wrong datatype in schema.js some installations using mysql could have been created using the
|
|
32
|
-
// data type text instead of mediumtext.
|
|
33
|
-
// For details see: https://github.com/TryGhost/Ghost/issues/1947
|
|
34
|
-
const checkPostTable = function checkPostTable(transaction = db.knex) {
|
|
35
|
-
return transaction.raw('SHOW FIELDS FROM posts where Field ="html" OR Field = "markdown"').then(function (response) {
|
|
36
|
-
return _.flatten(_.map(response[0], function (entry) {
|
|
37
|
-
if (entry.Type.toLowerCase() !== 'mediumtext') {
|
|
38
|
-
return (transaction || db.knex).raw('ALTER TABLE posts MODIFY ' + entry.Field + ' MEDIUMTEXT');
|
|
39
|
-
}
|
|
40
|
-
}));
|
|
41
|
-
});
|
|
42
|
-
};
|
|
43
|
-
|
|
44
30
|
module.exports = {
|
|
45
|
-
checkPostTable: checkPostTable,
|
|
46
31
|
getTables: getTables,
|
|
47
32
|
getIndexes: getIndexes,
|
|
48
33
|
getColumns: getColumns
|
|
@@ -411,14 +411,6 @@ function getColumns(table, transaction = db.knex) {
|
|
|
411
411
|
return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
-
function checkTables(transaction = db.knex) {
|
|
415
|
-
const client = transaction.client.config.client;
|
|
416
|
-
|
|
417
|
-
if (DatabaseInfo.isMySQL(transaction)) {
|
|
418
|
-
return clients[client].checkPostTable();
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
414
|
function createColumnMigration(...migrations) {
|
|
423
415
|
async function runColumnMigration(conn, migration) {
|
|
424
416
|
const {
|
|
@@ -449,7 +441,6 @@ function createColumnMigration(...migrations) {
|
|
|
449
441
|
}
|
|
450
442
|
|
|
451
443
|
module.exports = {
|
|
452
|
-
checkTables: checkTables,
|
|
453
444
|
createTable: createTable,
|
|
454
445
|
deleteTable: deleteTable,
|
|
455
446
|
getTables: getTables,
|
|
@@ -319,7 +319,7 @@ module.exports = {
|
|
|
319
319
|
maxlength: 50,
|
|
320
320
|
nullable: false,
|
|
321
321
|
defaultTo: 'custom',
|
|
322
|
-
validations: {isIn: [['internal', 'builtin', 'custom']]}
|
|
322
|
+
validations: {isIn: [['internal', 'builtin', 'custom', 'core']]}
|
|
323
323
|
},
|
|
324
324
|
name: {type: 'string', maxlength: 191, nullable: false},
|
|
325
325
|
slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
|
|
@@ -440,9 +440,9 @@ module.exports = {
|
|
|
440
440
|
stripe_coupon_id: {type: 'string', maxlength: 255, nullable: true, unique: true},
|
|
441
441
|
interval: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['month', 'year']]}},
|
|
442
442
|
currency: {type: 'string', maxlength: 50, nullable: true},
|
|
443
|
-
discount_type: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['percent', 'amount']]}},
|
|
443
|
+
discount_type: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['percent', 'amount', 'trial']]}},
|
|
444
444
|
discount_amount: {type: 'integer', nullable: false},
|
|
445
|
-
duration: {type: 'string', maxlength: 50, nullable: false},
|
|
445
|
+
duration: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['trial', 'once', 'repeating', 'forever']]}},
|
|
446
446
|
duration_in_months: {type: 'integer', nullable: true},
|
|
447
447
|
portal_title: {type: 'string', maxlength: 191, nullable: true},
|
|
448
448
|
portal_description: {type: 'string', maxlength: 2000, nullable: true},
|
|
@@ -33,7 +33,7 @@ module.exports = function (Bookshelf) {
|
|
|
33
33
|
options = options || {};
|
|
34
34
|
options.context = options.context || {};
|
|
35
35
|
|
|
36
|
-
if (options.context.user
|
|
36
|
+
if (options.context.user) {
|
|
37
37
|
return options.context.user;
|
|
38
38
|
} else if (options.context.integration) {
|
|
39
39
|
/**
|
|
@@ -64,8 +64,6 @@ module.exports = function (Bookshelf) {
|
|
|
64
64
|
return Bookshelf.Model.internalUser;
|
|
65
65
|
} else if (this.get('id')) {
|
|
66
66
|
return this.get('id');
|
|
67
|
-
} else if (options.context.external) {
|
|
68
|
-
return Bookshelf.Model.externalUser;
|
|
69
67
|
} else {
|
|
70
68
|
throw new errors.NotFoundError({
|
|
71
69
|
message: tpl(messages.missingContext),
|
|
@@ -80,15 +78,9 @@ module.exports = function (Bookshelf) {
|
|
|
80
78
|
* context.user ? true : false (if context.user is 0 as number, this condition is false)
|
|
81
79
|
*/
|
|
82
80
|
internalUser: 1,
|
|
83
|
-
externalUser: 0,
|
|
84
81
|
|
|
85
82
|
isInternalUser: function isInternalUser(id) {
|
|
86
83
|
return id === Bookshelf.Model.internalUser || id === Bookshelf.Model.internalUser.toString();
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
isExternalUser: function isExternalUser(id) {
|
|
90
|
-
return id === Bookshelf.Model.externalUser || id === Bookshelf.Model.externalUser.toString();
|
|
91
84
|
}
|
|
92
|
-
|
|
93
85
|
});
|
|
94
86
|
};
|
|
@@ -8,8 +8,7 @@ const messages = {
|
|
|
8
8
|
emptyComment: 'The body of a comment cannot be empty',
|
|
9
9
|
commentNotFound: 'Comment could not be found',
|
|
10
10
|
notYourCommentToEdit: 'You may only edit your own comments',
|
|
11
|
-
notYourCommentToDestroy: 'You may only delete your own comments'
|
|
12
|
-
cannotEditDeletedComment: 'You may only edit published comments'
|
|
11
|
+
notYourCommentToDestroy: 'You may only delete your own comments'
|
|
13
12
|
};
|
|
14
13
|
|
|
15
14
|
/**
|
|
@@ -52,7 +51,10 @@ const Comment = ghostBookshelf.Model.extend({
|
|
|
52
51
|
},
|
|
53
52
|
|
|
54
53
|
replies() {
|
|
55
|
-
return this.hasMany('Comment', 'parent_id')
|
|
54
|
+
return this.hasMany('Comment', 'parent_id', 'id')
|
|
55
|
+
.query('orderBy', 'created_at', 'ASC')
|
|
56
|
+
// Note: this limit is not working
|
|
57
|
+
.query('limit', 3);
|
|
56
58
|
},
|
|
57
59
|
|
|
58
60
|
emitChange: function emitChange(event, options) {
|
|
@@ -63,12 +65,6 @@ const Comment = ghostBookshelf.Model.extend({
|
|
|
63
65
|
onSaving() {
|
|
64
66
|
ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
|
|
65
67
|
|
|
66
|
-
if (this.hasChanged('html') && this.get('status') !== 'published') {
|
|
67
|
-
throw new ValidationError({
|
|
68
|
-
message: tpl(messages.cannotEditDeletedComment)
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
68
|
if (this.hasChanged('html')) {
|
|
73
69
|
const sanitizeHtml = require('sanitize-html');
|
|
74
70
|
|
|
@@ -105,13 +101,16 @@ const Comment = ghostBookshelf.Model.extend({
|
|
|
105
101
|
},
|
|
106
102
|
|
|
107
103
|
enforcedFilters: function enforcedFilters(options) {
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
// Convenicence option to merge all filters with parent_id:null filter
|
|
105
|
+
if (options.parentId !== undefined) {
|
|
106
|
+
if (options.parentId === null) {
|
|
107
|
+
return 'parent_id:null';
|
|
108
|
+
}
|
|
109
|
+
return 'parent_id:' + options.parentId;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
return
|
|
112
|
+
return null;
|
|
113
113
|
}
|
|
114
|
-
|
|
115
114
|
}, {
|
|
116
115
|
destroy: function destroy(unfilteredOptions) {
|
|
117
116
|
let options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
|
|
@@ -181,12 +180,94 @@ const Comment = ghostBookshelf.Model.extend({
|
|
|
181
180
|
*/
|
|
182
181
|
defaultRelations: function defaultRelations(methodName, options) {
|
|
183
182
|
// @todo: the default relations are not working for 'add' when we add it below
|
|
184
|
-
if (['findAll', 'findPage', 'edit', 'findOne'].indexOf(methodName) !== -1) {
|
|
183
|
+
if (['findAll', 'findPage', 'edit', 'findOne', 'destroy'].indexOf(methodName) !== -1) {
|
|
185
184
|
if (!options.withRelated || options.withRelated.length === 0) {
|
|
186
|
-
options.
|
|
185
|
+
if (options.parentId) {
|
|
186
|
+
// Do not include replies for replies
|
|
187
|
+
options.withRelated = [
|
|
188
|
+
// Relations
|
|
189
|
+
'member', 'count.likes', 'count.liked'
|
|
190
|
+
];
|
|
191
|
+
} else {
|
|
192
|
+
options.withRelated = [
|
|
193
|
+
// Relations
|
|
194
|
+
'member', 'count.replies', 'count.likes', 'count.liked',
|
|
195
|
+
// Replies (limited to 3)
|
|
196
|
+
'replies', 'replies.member' , 'replies.count.likes', 'replies.count.liked'
|
|
197
|
+
];
|
|
198
|
+
}
|
|
187
199
|
}
|
|
188
200
|
}
|
|
189
201
|
|
|
202
|
+
return options;
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
async findPage(options) {
|
|
206
|
+
const {withRelated} = this.defaultRelations('findPage', options);
|
|
207
|
+
|
|
208
|
+
const relationsToLoadIndividually = [
|
|
209
|
+
'replies',
|
|
210
|
+
'replies.member',
|
|
211
|
+
'replies.count.likes',
|
|
212
|
+
'replies.count.liked'
|
|
213
|
+
].filter(relation => withRelated.includes(relation));
|
|
214
|
+
|
|
215
|
+
const result = await ghostBookshelf.Model.findPage.call(this, options);
|
|
216
|
+
|
|
217
|
+
for (const model of result.data) {
|
|
218
|
+
await model.load(relationsToLoadIndividually, _.omit(options, 'withRelated'));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
countRelations() {
|
|
225
|
+
return {
|
|
226
|
+
replies(modelOrCollection) {
|
|
227
|
+
modelOrCollection.query('columns', 'comments.*', (qb) => {
|
|
228
|
+
qb.count('replies.id')
|
|
229
|
+
.from('comments AS replies')
|
|
230
|
+
.whereRaw('replies.parent_id = comments.id')
|
|
231
|
+
.as('count__replies');
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
likes(modelOrCollection) {
|
|
235
|
+
modelOrCollection.query('columns', 'comments.*', (qb) => {
|
|
236
|
+
qb.count('comment_likes.id')
|
|
237
|
+
.from('comment_likes')
|
|
238
|
+
.whereRaw('comment_likes.comment_id = comments.id')
|
|
239
|
+
.as('count__likes');
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
liked(modelOrCollection, options) {
|
|
243
|
+
modelOrCollection.query('columns', 'comments.*', (qb) => {
|
|
244
|
+
if (options.context && options.context.member && options.context.member.id) {
|
|
245
|
+
qb.count('comment_likes.id')
|
|
246
|
+
.from('comment_likes')
|
|
247
|
+
.whereRaw('comment_likes.comment_id = comments.id')
|
|
248
|
+
.where('comment_likes.member_id', options.context.member.id)
|
|
249
|
+
.as('count__liked');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Return zero
|
|
254
|
+
qb.select(ghostBookshelf.knex.raw('0')).as('count__liked');
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Returns an array of keys permitted in a method's `options` hash, depending on the current method.
|
|
262
|
+
* @param {String} methodName The name of the method to check valid options for.
|
|
263
|
+
* @return {Array} Keys allowed in the `options` hash of the model's method.
|
|
264
|
+
*/
|
|
265
|
+
permittedOptions: function permittedOptions(methodName) {
|
|
266
|
+
let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
|
|
267
|
+
|
|
268
|
+
// The comment model additionally supports having a parentId option
|
|
269
|
+
options.push('parentId');
|
|
270
|
+
|
|
190
271
|
return options;
|
|
191
272
|
}
|
|
192
273
|
});
|
|
@@ -107,6 +107,20 @@ Label = ghostBookshelf.Model.extend({
|
|
|
107
107
|
return options;
|
|
108
108
|
},
|
|
109
109
|
|
|
110
|
+
countRelations() {
|
|
111
|
+
return {
|
|
112
|
+
members(modelOrCollection) {
|
|
113
|
+
modelOrCollection.query('columns', 'labels.*', (qb) => {
|
|
114
|
+
qb.count('members.id')
|
|
115
|
+
.from('members')
|
|
116
|
+
.leftOuterJoin('members_labels', 'members.id', 'members_labels.member_id')
|
|
117
|
+
.whereRaw('members_labels.label_id = labels.id')
|
|
118
|
+
.as('count__members');
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
|
|
110
124
|
destroy: function destroy(unfilteredOptions) {
|
|
111
125
|
const options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
|
|
112
126
|
options.withRelated = ['members'];
|
|
@@ -126,6 +126,27 @@ const Newsletter = ghostBookshelf.Model.extend({
|
|
|
126
126
|
return options;
|
|
127
127
|
},
|
|
128
128
|
|
|
129
|
+
countRelations() {
|
|
130
|
+
return {
|
|
131
|
+
posts(modelOrCollection) {
|
|
132
|
+
modelOrCollection.query('columns', 'newsletters.*', (qb) => {
|
|
133
|
+
qb.count('posts.id')
|
|
134
|
+
.from('posts')
|
|
135
|
+
.whereRaw('posts.newsletter_id = newsletters.id')
|
|
136
|
+
.as('count__posts');
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
members(modelOrCollection) {
|
|
140
|
+
modelOrCollection.query('columns', 'newsletters.*', (qb) => {
|
|
141
|
+
qb.count('members_newsletters.id')
|
|
142
|
+
.from('members_newsletters')
|
|
143
|
+
.whereRaw('members_newsletters.newsletter_id = newsletters.id')
|
|
144
|
+
.as('count__members');
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
|
|
129
150
|
orderDefaultRaw: function () {
|
|
130
151
|
return 'sort_order ASC, created_at ASC, id ASC';
|
|
131
152
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const ghostBookshelf = require('./base');
|
|
2
2
|
const _ = require('lodash');
|
|
3
|
+
const labs = require('../../shared/labs');
|
|
3
4
|
|
|
4
5
|
const StripeCustomerSubscription = ghostBookshelf.Model.extend({
|
|
5
6
|
tableName: 'members_stripe_customers_subscriptions',
|
|
@@ -42,6 +43,11 @@ const StripeCustomerSubscription = ghostBookshelf.Model.extend({
|
|
|
42
43
|
current_period_end: defaultSerializedObject.current_period_end
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
if (labs.isSet('freeTrial')) {
|
|
47
|
+
serialized.trial_start_at = defaultSerializedObject.trial_start_at;
|
|
48
|
+
serialized.trial_end_at = defaultSerializedObject.trial_end_at;
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
if (!_.isEmpty(defaultSerializedObject.stripePrice)) {
|
|
46
52
|
serialized.price = {
|
|
47
53
|
id: defaultSerializedObject.stripePrice.stripe_price_id,
|
|
@@ -182,6 +182,26 @@ Tag = ghostBookshelf.Model.extend({
|
|
|
182
182
|
return options;
|
|
183
183
|
},
|
|
184
184
|
|
|
185
|
+
countRelations() {
|
|
186
|
+
return {
|
|
187
|
+
posts(modelOrCollection, options) {
|
|
188
|
+
modelOrCollection.query('columns', 'tags.*', (qb) => {
|
|
189
|
+
qb.count('posts.id')
|
|
190
|
+
.from('posts')
|
|
191
|
+
.leftOuterJoin('posts_tags', 'posts.id', 'posts_tags.post_id')
|
|
192
|
+
.whereRaw('posts_tags.tag_id = tags.id')
|
|
193
|
+
.as('count__posts');
|
|
194
|
+
|
|
195
|
+
if (options.context && options.context.public) {
|
|
196
|
+
// @TODO use the filter behavior for posts
|
|
197
|
+
qb.andWhere('posts.type', '=', 'post');
|
|
198
|
+
qb.andWhere('posts.status', '=', 'published');
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
|
|
185
205
|
destroy: function destroy(unfilteredOptions) {
|
|
186
206
|
const options = this.filterOptions(unfilteredOptions, 'destroy', {extraAllowedProperties: ['id']});
|
|
187
207
|
options.withRelated = ['posts'];
|
|
@@ -426,6 +426,26 @@ User = ghostBookshelf.Model.extend({
|
|
|
426
426
|
return permittedOptionsToReturn;
|
|
427
427
|
},
|
|
428
428
|
|
|
429
|
+
countRelations() {
|
|
430
|
+
return {
|
|
431
|
+
posts(modelOrCollection, options) {
|
|
432
|
+
modelOrCollection.query('columns', 'users.*', (qb) => {
|
|
433
|
+
qb.count('posts.id')
|
|
434
|
+
.from('posts')
|
|
435
|
+
.join('posts_authors', 'posts.id', 'posts_authors.post_id')
|
|
436
|
+
.whereRaw('posts_authors.author_id = users.id')
|
|
437
|
+
.as('count__posts');
|
|
438
|
+
|
|
439
|
+
if (options.context && options.context.public) {
|
|
440
|
+
// @TODO use the filter behavior for posts
|
|
441
|
+
qb.andWhere('posts.type', '=', 'post');
|
|
442
|
+
qb.andWhere('posts.status', '=', 'published');
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
},
|
|
448
|
+
|
|
429
449
|
/**
|
|
430
450
|
* ### Find One
|
|
431
451
|
*
|
|
@@ -127,7 +127,7 @@ const authenticateWithToken = async (req, res, next, {token, JWT_OPTIONS}) => {
|
|
|
127
127
|
|
|
128
128
|
// CASE: blocking all non-internal: "custom" and "builtin" integration requests when the limit is reached
|
|
129
129
|
if (limitService.isLimited('customIntegrations')
|
|
130
|
-
&& (apiKey.relations.integration && !['internal'].includes(apiKey.relations.integration.get('type')))) {
|
|
130
|
+
&& (apiKey.relations.integration && !['internal', 'core'].includes(apiKey.relations.integration.get('type')))) {
|
|
131
131
|
// NOTE: using "checkWouldGoOverLimit" instead of "checkIsOverLimit" here because flag limits don't have
|
|
132
132
|
// a concept of measuring if the limit has been surpassed
|
|
133
133
|
await limitService.errorIfWouldGoOverLimit('customIntegrations');
|
|
@@ -43,7 +43,7 @@ const authenticateContentApiKey = async function authenticateContentApiKey(req,
|
|
|
43
43
|
|
|
44
44
|
// CASE: blocking all non-internal: "custom" and "builtin" integration requests when the limit is reached
|
|
45
45
|
if (limitService.isLimited('customIntegrations')
|
|
46
|
-
&& (apiKey.relations.integration && !['internal'].includes(apiKey.relations.integration.get('type')))) {
|
|
46
|
+
&& (apiKey.relations.integration && !['internal', 'core'].includes(apiKey.relations.integration.get('type')))) {
|
|
47
47
|
// NOTE: using "checkWouldGoOverLimit" instead of "checkIsOverLimit" here because flag limits don't have
|
|
48
48
|
// a concept of measuring if the limit has been surpassed
|
|
49
49
|
await limitService.errorIfWouldGoOverLimit('customIntegrations');
|
|
@@ -5,17 +5,19 @@ const errors = require('@tryghost/errors');
|
|
|
5
5
|
const tpl = require('@tryghost/tpl');
|
|
6
6
|
const logging = require('@tryghost/logging');
|
|
7
7
|
const models = require('../../models');
|
|
8
|
-
const
|
|
8
|
+
const MailgunClient = require('@tryghost/mailgun-client');
|
|
9
9
|
const sentry = require('../../../shared/sentry');
|
|
10
10
|
const labs = require('../../../shared/labs');
|
|
11
11
|
const debug = require('@tryghost/debug')('mega');
|
|
12
12
|
const postEmailSerializer = require('../mega/post-email-serializer');
|
|
13
|
+
const configService = require('../../../shared/config');
|
|
14
|
+
const settingsCache = require('../../../shared/settings-cache');
|
|
13
15
|
|
|
14
16
|
const messages = {
|
|
15
17
|
error: 'The email service was unable to send an email batch.'
|
|
16
18
|
};
|
|
17
19
|
|
|
18
|
-
const
|
|
20
|
+
const mailgunClient = new MailgunClient({config: configService, settings: settingsCache});
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* An object representing batch request result
|
|
@@ -64,7 +66,7 @@ class FailedBatch extends BatchResultBase {
|
|
|
64
66
|
*/
|
|
65
67
|
|
|
66
68
|
module.exports = {
|
|
67
|
-
BATCH_SIZE,
|
|
69
|
+
BATCH_SIZE: MailgunClient.BATCH_SIZE,
|
|
68
70
|
SuccessfulBatch,
|
|
69
71
|
FailedBatch,
|
|
70
72
|
|
|
@@ -211,11 +213,12 @@ module.exports = {
|
|
|
211
213
|
* @param {Email-like} emailData - The email to send, must be a POJO so emailModel.toJSON() before calling if needed
|
|
212
214
|
* @param {[EmailRecipient]} recipients - The recipients to send the email to with their associated data
|
|
213
215
|
* @param {string?} memberSegment - The member segment of the recipients
|
|
214
|
-
* @returns {Object} - {providerId: 'xxx'}
|
|
216
|
+
* @returns {Promise<Object>} - {providerId: 'xxx'}
|
|
215
217
|
*/
|
|
216
|
-
send(emailData, recipients, memberSegment) {
|
|
217
|
-
const
|
|
218
|
-
if (!
|
|
218
|
+
async send(emailData, recipients, memberSegment) {
|
|
219
|
+
const mailgunConfigured = mailgunClient.isConfigured();
|
|
220
|
+
if (!mailgunConfigured) {
|
|
221
|
+
logging.warn('Bulk email has not been configured');
|
|
219
222
|
return;
|
|
220
223
|
}
|
|
221
224
|
|
|
@@ -247,10 +250,11 @@ module.exports = {
|
|
|
247
250
|
|
|
248
251
|
emailData = postEmailSerializer.renderEmailForSegment(emailData, memberSegment);
|
|
249
252
|
|
|
250
|
-
|
|
253
|
+
try {
|
|
254
|
+
const response = await mailgunClient.send(emailData, recipientData, replacements);
|
|
251
255
|
debug(`sent message (${Date.now() - startTime}ms)`);
|
|
252
256
|
return response;
|
|
253
|
-
}
|
|
257
|
+
} catch (error) {
|
|
254
258
|
// REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
|
|
255
259
|
let ghostError = new errors.EmailError({
|
|
256
260
|
err: error,
|
|
@@ -263,6 +267,9 @@ module.exports = {
|
|
|
263
267
|
|
|
264
268
|
debug(`failed to send message (${Date.now() - startTime}ms)`);
|
|
265
269
|
throw ghostError;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
// NOTE: for testing only!
|
|
274
|
+
_mailgunClient: mailgunClient
|
|
268
275
|
};
|
|
@@ -1,17 +1 @@
|
|
|
1
|
-
|
|
2
|
-
BATCH_SIZE,
|
|
3
|
-
SuccessfulBatch,
|
|
4
|
-
FailedBatch,
|
|
5
|
-
processEmail,
|
|
6
|
-
processEmailBatch,
|
|
7
|
-
send
|
|
8
|
-
} = require('./bulk-email-processor');
|
|
9
|
-
|
|
10
|
-
module.exports = {
|
|
11
|
-
BATCH_SIZE,
|
|
12
|
-
SuccessfulBatch,
|
|
13
|
-
FailedBatch,
|
|
14
|
-
processEmail,
|
|
15
|
-
processEmailBatch,
|
|
16
|
-
send
|
|
17
|
-
};
|
|
1
|
+
module.exports = require('./bulk-email-processor');
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @typedef {import('../../api/shared/frame')} Frame
|
|
3
5
|
*/
|
|
@@ -26,6 +28,13 @@ module.exports = class CommentsController {
|
|
|
26
28
|
return this.service.getComments(frame.options);
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* @param {Frame} frame
|
|
33
|
+
*/
|
|
34
|
+
async replies(frame) {
|
|
35
|
+
return this.service.getReplies(frame.options.id, _.omit(frame.options, 'id'));
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
/**
|
|
30
39
|
* @param {Frame} frame
|
|
31
40
|
*/
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
border-bottom-color: #EEF5F8;
|
|
95
95
|
}
|
|
96
96
|
a {
|
|
97
|
-
color: #
|
|
97
|
+
color: #15212A;
|
|
98
98
|
}
|
|
99
99
|
</style>
|
|
100
100
|
</head>
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
<tr>
|
|
117
117
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
|
118
118
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Hey there,</p>
|
|
119
|
-
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">Someone just replied to your comment on <a href="{{postUrl}}" target="_blank" style="font-weight: bold; text-decoration: underline;">{{postTitle}}</a>.</p>
|
|
119
|
+
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">Someone just replied to your comment on <a href="{{postUrl}}" target="_blank" style="font-weight: bold; text-decoration: underline; color: #15212A;">{{postTitle}}</a>.</p>
|
|
120
120
|
|
|
121
121
|
<table width="100" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F9F9FA; border-radius: 7px;">
|
|
122
122
|
<tbody>
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
border-bottom-color: #EEF5F8;
|
|
95
95
|
}
|
|
96
96
|
a {
|
|
97
|
-
color: #
|
|
97
|
+
color: #15212A;
|
|
98
98
|
}
|
|
99
99
|
</style>
|
|
100
100
|
</head>
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
<tr>
|
|
117
117
|
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
|
118
118
|
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Hey there,</p>
|
|
119
|
-
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">Someone just left a comment on your post <a href="{{postUrl}}" target="_blank" style="font-weight: bold; text-decoration: underline;">{{postTitle}}</a>.</p>
|
|
119
|
+
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">Someone just left a comment on your post <a href="{{postUrl}}" target="_blank" style="font-weight: bold; text-decoration: underline; color: #15212A;">{{postTitle}}</a>.</p>
|
|
120
120
|
|
|
121
121
|
<table width="100" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F9F9FA; border-radius: 7px;">
|
|
122
122
|
<tbody>
|