ghost 5.31.0 → 5.33.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-5.31.0.tgz → tryghost-adapter-manager-5.33.0.tgz} +0 -0
- package/components/{tryghost-api-framework-5.31.0.tgz → tryghost-api-framework-5.33.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.31.0.tgz → tryghost-api-version-compatibility-service-5.33.0.tgz} +0 -0
- package/components/tryghost-audience-feedback-5.33.0.tgz +0 -0
- package/components/{tryghost-bootstrap-socket-5.31.0.tgz → tryghost-bootstrap-socket-5.33.0.tgz} +0 -0
- package/components/tryghost-constants-5.33.0.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.31.0.tgz → tryghost-custom-theme-settings-service-5.33.0.tgz} +0 -0
- package/components/tryghost-data-generator-5.33.0.tgz +0 -0
- package/components/tryghost-domain-events-5.33.0.tgz +0 -0
- package/components/tryghost-dynamic-routing-events-5.33.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.33.0.tgz +0 -0
- package/components/{tryghost-email-analytics-service-5.31.0.tgz → tryghost-email-analytics-service-5.33.0.tgz} +0 -0
- package/components/tryghost-email-content-generator-5.33.0.tgz +0 -0
- package/components/{tryghost-email-events-5.31.0.tgz → tryghost-email-events-5.33.0.tgz} +0 -0
- package/components/tryghost-email-service-5.33.0.tgz +0 -0
- package/components/{tryghost-email-suppression-list-5.31.0.tgz → tryghost-email-suppression-list-5.33.0.tgz} +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.31.0.tgz → tryghost-express-dynamic-redirects-5.33.0.tgz} +0 -0
- package/components/{tryghost-extract-api-key-5.31.0.tgz → tryghost-extract-api-key-5.33.0.tgz} +0 -0
- package/components/tryghost-html-to-plaintext-5.33.0.tgz +0 -0
- package/components/tryghost-i18n-5.33.0.tgz +0 -0
- package/components/{tryghost-importer-revue-5.31.0.tgz → tryghost-importer-revue-5.33.0.tgz} +0 -0
- package/components/{tryghost-job-manager-5.31.0.tgz → tryghost-job-manager-5.33.0.tgz} +0 -0
- package/components/{tryghost-link-redirects-5.31.0.tgz → tryghost-link-redirects-5.33.0.tgz} +0 -0
- package/components/tryghost-link-replacer-5.33.0.tgz +0 -0
- package/components/{tryghost-link-tracking-5.31.0.tgz → tryghost-link-tracking-5.33.0.tgz} +0 -0
- package/components/{tryghost-magic-link-5.31.0.tgz → tryghost-magic-link-5.33.0.tgz} +0 -0
- package/components/tryghost-mailgun-client-5.33.0.tgz +0 -0
- package/components/{tryghost-member-attribution-5.31.0.tgz → tryghost-member-attribution-5.33.0.tgz} +0 -0
- package/components/{tryghost-member-events-5.31.0.tgz → tryghost-member-events-5.33.0.tgz} +0 -0
- package/components/{tryghost-members-api-5.31.0.tgz → tryghost-members-api-5.33.0.tgz} +0 -0
- package/components/{tryghost-members-csv-5.31.0.tgz → tryghost-members-csv-5.33.0.tgz} +0 -0
- package/components/{tryghost-members-events-service-5.31.0.tgz → tryghost-members-events-service-5.33.0.tgz} +0 -0
- package/components/{tryghost-members-importer-5.31.0.tgz → tryghost-members-importer-5.33.0.tgz} +0 -0
- package/components/{tryghost-members-offers-5.31.0.tgz → tryghost-members-offers-5.33.0.tgz} +0 -0
- package/components/tryghost-members-payments-5.33.0.tgz +0 -0
- package/components/{tryghost-members-ssr-5.31.0.tgz → tryghost-members-ssr-5.33.0.tgz} +0 -0
- package/components/tryghost-members-stripe-service-5.33.0.tgz +0 -0
- package/components/{tryghost-minifier-5.31.0.tgz → tryghost-minifier-5.33.0.tgz} +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.33.0.tgz +0 -0
- package/components/{tryghost-mw-cache-control-5.31.0.tgz → tryghost-mw-cache-control-5.33.0.tgz} +0 -0
- package/components/{tryghost-mw-error-handler-5.31.0.tgz → tryghost-mw-error-handler-5.33.0.tgz} +0 -0
- package/components/tryghost-mw-session-from-token-5.33.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.33.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.33.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.33.0.tgz +0 -0
- package/components/{tryghost-package-json-5.31.0.tgz → tryghost-package-json-5.33.0.tgz} +0 -0
- package/components/tryghost-referrers-5.33.0.tgz +0 -0
- package/components/{tryghost-security-5.31.0.tgz → tryghost-security-5.33.0.tgz} +0 -0
- package/components/tryghost-session-service-5.33.0.tgz +0 -0
- package/components/{tryghost-settings-path-manager-5.31.0.tgz → tryghost-settings-path-manager-5.33.0.tgz} +0 -0
- package/components/tryghost-staff-service-5.33.0.tgz +0 -0
- package/components/tryghost-stats-service-5.33.0.tgz +0 -0
- package/components/{tryghost-tiers-5.31.0.tgz → tryghost-tiers-5.33.0.tgz} +0 -0
- package/components/{tryghost-update-check-service-5.31.0.tgz → tryghost-update-check-service-5.33.0.tgz} +0 -0
- package/components/tryghost-verification-trigger-5.33.0.tgz +0 -0
- package/components/{tryghost-version-notifications-data-service-5.31.0.tgz → tryghost-version-notifications-data-service-5.33.0.tgz} +0 -0
- package/components/tryghost-webmentions-5.33.0.tgz +0 -0
- package/core/built/admin/assets/{chunk.143.7ef8d39b50a9ef3d6a6b.js → chunk.143.e158db597c343f4f132e.js} +23 -23
- package/core/built/admin/assets/{chunk.178.8a8e070a8c2682df548a.js → chunk.178.eaeb178bc4597cb87699.js} +4 -4
- package/core/built/admin/assets/{chunk.507.aa38cb08f25bae1d4e76.js → chunk.79.c3c2c05ea7ff7707fcad.js} +173 -167
- package/core/built/admin/assets/{chunk.47.f29250a4560868c21293.js → chunk.963.e47ead5abeca4cf69fed.js} +1001 -1007
- package/core/built/admin/assets/{chunk.47.f29250a4560868c21293.js.LICENSE.txt → chunk.963.e47ead5abeca4cf69fed.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/{ghost-fc0450a45ea5be2e5a10c4d897d5b430.js → ghost-86f1db1d00d5f0bc4f0cbdb39ff6c977.js} +135 -119
- package/core/built/admin/assets/{ghost-dark-fb05eb50e216469c5626356731afa42f.css → ghost-dark-ac0cb221eddc8652a0e7c263ed6513dc.css} +1 -1
- package/core/built/admin/assets/{ghost-721a7adc4ca642c88e4ac85e1cb8b385.css → ghost-fb9fb8adbcaf1603ad4006dd2d49e401.css} +1 -1
- package/core/built/admin/assets/img/stripe-3af79c77413d7e58d0322c1de5bfdb8e.svg +1 -0
- package/core/built/admin/index.html +5 -5
- package/core/cli/generate-data.js +3 -1
- package/core/frontend/services/sitemap/base-generator.js +16 -1
- package/core/frontend/services/sitemap/manager.js +6 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/emails.js +8 -0
- package/core/server/data/migrations/versions/5.32/2023-01-24-08-00-fix-invalid-tier-expiry-for-paid-members.js +35 -0
- package/core/server/data/migrations/versions/5.32/2023-01-24-08-09-restore-incorrect-expired-tiers-for-members.js +46 -0
- package/core/server/models/member.js +2 -2
- package/core/server/services/bulk-email/bulk-email-processor.js +7 -4
- package/core/server/services/email-analytics/jobs/index.js +3 -3
- package/core/server/services/email-service/wrapper.js +5 -2
- package/core/server/services/mentions/BookshelfMentionRepository.js +8 -0
- package/core/server/services/mentions/ResourceService.js +45 -0
- package/core/server/services/mentions/RoutingService.js +78 -0
- package/core/server/services/mentions/service.js +19 -32
- package/core/server/services/staff/index.js +4 -1
- package/core/server/services/url/Resources.js +33 -0
- package/core/server/services/url/Urls.js +3 -0
- package/core/server/services/url/config.js +6 -3
- package/core/shared/labs.js +3 -1
- package/package.json +110 -108
- package/yarn.lock +129 -129
- package/components/tryghost-audience-feedback-5.31.0.tgz +0 -0
- package/components/tryghost-constants-5.31.0.tgz +0 -0
- package/components/tryghost-data-generator-5.31.0.tgz +0 -0
- package/components/tryghost-domain-events-5.31.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.31.0.tgz +0 -0
- package/components/tryghost-email-content-generator-5.31.0.tgz +0 -0
- package/components/tryghost-email-service-5.31.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.31.0.tgz +0 -0
- package/components/tryghost-i18n-5.31.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.31.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.31.0.tgz +0 -0
- package/components/tryghost-members-payments-5.31.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.31.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.31.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.31.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.31.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.31.0.tgz +0 -0
- package/components/tryghost-oembed-service-5.31.0.tgz +0 -0
- package/components/tryghost-referrers-5.31.0.tgz +0 -0
- package/components/tryghost-session-service-5.31.0.tgz +0 -0
- package/components/tryghost-staff-service-5.31.0.tgz +0 -0
- package/components/tryghost-stats-service-5.31.0.tgz +0 -0
- package/components/tryghost-verification-trigger-5.31.0.tgz +0 -0
- package/components/tryghost-webmentions-5.31.0.tgz +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" style="enable-background:new 0 0 400 400" xml:space="preserve"><path style="fill-rule:evenodd;clip-rule:evenodd;fill:#635bff" d="M0 0h400v400H0z"/><path d="M184.4 155.5c0-9.4 7.7-13.1 20.5-13.1 18.4 0 41.6 5.6 60 15.5v-56.8C244.8 93.1 225 90 205 90c-49.1 0-81.7 25.6-81.7 68.4 0 66.7 91.9 56.1 91.9 84.9 0 11.1-9.7 14.7-23.2 14.7-20.1 0-45.7-8.2-66-19.3v57.5c22.5 9.7 45.2 13.8 66 13.8 50.3 0 84.9-24.9 84.9-68.2-.4-72-92.5-59.2-92.5-86.3z" style="fill-rule:evenodd;clip-rule:evenodd;fill:#fff"/></svg>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<title>Ghost Admin</title>
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.
|
|
11
|
+
<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.33%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
|
|
12
12
|
|
|
13
13
|
<meta name="HandheldFriendly" content="True" />
|
|
14
14
|
<meta name="MobileOptimized" content="320" />
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
</style>
|
|
38
38
|
|
|
39
39
|
<link integrity="" rel="stylesheet" href="assets/vendor-3e6947aa681f0fb82b193090e520dc73.css">
|
|
40
|
-
<link integrity="" rel="stylesheet" href="assets/ghost-
|
|
40
|
+
<link integrity="" rel="stylesheet" href="assets/ghost-fb9fb8adbcaf1603ad4006dd2d49e401.css" title="light">
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
</head>
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
<div id="ember-basic-dropdown-wormhole"></div>
|
|
58
58
|
|
|
59
59
|
<script src="assets/vendor-0441964c34d58f2aacd5a04bbe240243.js"></script>
|
|
60
|
-
<script src="assets/chunk.
|
|
61
|
-
<script src="assets/chunk.143.
|
|
62
|
-
<script src="assets/ghost-
|
|
60
|
+
<script src="assets/chunk.963.e47ead5abeca4cf69fed.js"></script>
|
|
61
|
+
<script src="assets/chunk.143.e158db597c343f4f132e.js"></script>
|
|
62
|
+
<script src="assets/ghost-86f1db1d00d5f0bc4f0cbdb39ff6c977.js"></script>
|
|
63
63
|
</body>
|
|
64
64
|
</html>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const Command = require('./command');
|
|
2
2
|
const DataGenerator = require('@tryghost/data-generator');
|
|
3
|
+
const config = require('../shared/config');
|
|
3
4
|
|
|
4
5
|
module.exports = class REPL extends Command {
|
|
5
6
|
setup() {
|
|
@@ -39,7 +40,8 @@ module.exports = class REPL extends Command {
|
|
|
39
40
|
fatal: this.fatal,
|
|
40
41
|
debug: this.debug
|
|
41
42
|
},
|
|
42
|
-
modelQuantities: {}
|
|
43
|
+
modelQuantities: {},
|
|
44
|
+
baseUrl: config.getSiteUrl()
|
|
43
45
|
});
|
|
44
46
|
try {
|
|
45
47
|
await dataGenerator.importData();
|
|
@@ -55,7 +55,7 @@ class BaseSiteMapGenerator {
|
|
|
55
55
|
// Generate full xml
|
|
56
56
|
let sitemapXml = localUtils.getDeclarations() + xml(data);
|
|
57
57
|
|
|
58
|
-
// Perform url
|
|
58
|
+
// Perform url transformations
|
|
59
59
|
// - Necessary because sitemap data is supplied by the router which
|
|
60
60
|
// uses knex directly bypassing model-layer attribute transforms
|
|
61
61
|
sitemapXml = urlUtils.transformReadyToAbsolute(sitemapXml);
|
|
@@ -63,6 +63,15 @@ class BaseSiteMapGenerator {
|
|
|
63
63
|
return sitemapXml;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
updateURL(datum) {
|
|
67
|
+
const url = this.nodeLookup[datum.id]?.url[0].loc;
|
|
68
|
+
|
|
69
|
+
if (url) {
|
|
70
|
+
this.removeUrl(url, datum);
|
|
71
|
+
this.addUrl(url, datum);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
addUrl(url, datum) {
|
|
67
76
|
const node = this.createUrlNodeFromDatum(url, datum);
|
|
68
77
|
|
|
@@ -100,6 +109,12 @@ class BaseSiteMapGenerator {
|
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
111
|
|
|
112
|
+
/**
|
|
113
|
+
*
|
|
114
|
+
* @param {String} url
|
|
115
|
+
* @param {Object} datum
|
|
116
|
+
* @returns
|
|
117
|
+
*/
|
|
103
118
|
createUrlNodeFromDatum(url, datum) {
|
|
104
119
|
let node;
|
|
105
120
|
let imgNode;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const DomainEvents = require('@tryghost/domain-events');
|
|
2
|
+
const {URLResourceUpdatedEvent} = require('@tryghost/dynamic-routing-events');
|
|
1
3
|
const IndexMapGenerator = require('./index-generator');
|
|
2
4
|
const PagesMapGenerator = require('./page-generator');
|
|
3
5
|
const PostsMapGenerator = require('./post-generator');
|
|
@@ -29,6 +31,10 @@ class SiteMapManager {
|
|
|
29
31
|
}
|
|
30
32
|
});
|
|
31
33
|
|
|
34
|
+
DomainEvents.subscribe(URLResourceUpdatedEvent, (event) => {
|
|
35
|
+
this[event.data.resourceType].updateURL(event.data);
|
|
36
|
+
});
|
|
37
|
+
|
|
32
38
|
events.on('url.added', (obj) => {
|
|
33
39
|
this[obj.resource.config.type].addUrl(obj.url.absolute, obj.resource.data);
|
|
34
40
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const mega = require('../../../../../../services/mega');
|
|
2
|
+
const labs = require('../../../../../../../shared/labs');
|
|
3
|
+
const config = require('../../../../../../../shared/config');
|
|
2
4
|
|
|
3
5
|
module.exports = (model, frame) => {
|
|
4
6
|
const jsonModel = model.toJSON ? model.toJSON(frame.options) : model;
|
|
@@ -13,5 +15,11 @@ module.exports = (model, frame) => {
|
|
|
13
15
|
);
|
|
14
16
|
});
|
|
15
17
|
|
|
18
|
+
if (!labs.isSet('emailErrors') && !!(config.get('bulkEmail') && config.get('bulkEmail').mailgun)) {
|
|
19
|
+
if (jsonModel.status === 'failed') {
|
|
20
|
+
jsonModel.status = 'submitted';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
return jsonModel;
|
|
17
25
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
3
|
+
|
|
4
|
+
module.exports = createTransactionalMigration(
|
|
5
|
+
async function up(knex) {
|
|
6
|
+
logging.info('Removing expiry dates for paid members');
|
|
7
|
+
try {
|
|
8
|
+
// Fetch all members with a paid status that have an expiry date
|
|
9
|
+
// Paid members should not have an expiry date
|
|
10
|
+
const invalidExpiryIds = await knex('members_products')
|
|
11
|
+
.select('members_products.id')
|
|
12
|
+
.leftJoin('members', 'members_products.member_id', 'members.id')
|
|
13
|
+
.where('members.status', '=', 'paid')
|
|
14
|
+
.whereNotNull('members_products.expiry_at').pluck('members_products.id');
|
|
15
|
+
|
|
16
|
+
logging.info(`Found ${invalidExpiryIds.length} paid members with expiry dates`);
|
|
17
|
+
|
|
18
|
+
if (invalidExpiryIds.length === 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
logging.info(`Removing expiry dates for ${invalidExpiryIds.length} paid members`);
|
|
23
|
+
|
|
24
|
+
await knex('members_products')
|
|
25
|
+
.update('expiry_at', null)
|
|
26
|
+
.whereIn('id', invalidExpiryIds);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
logging.warn('Failed to remove expiry dates for paid members');
|
|
29
|
+
logging.warn(err);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
async function down() {
|
|
33
|
+
// no-op: we don't want to reintroduce the incorrect expiry dates for member tiers
|
|
34
|
+
}
|
|
35
|
+
);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const ObjectId = require('bson-objectid').default;
|
|
3
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = createTransactionalMigration(
|
|
6
|
+
async function up(knex) {
|
|
7
|
+
logging.info('Restoring member<>tier mapping for members with paid status');
|
|
8
|
+
try {
|
|
9
|
+
// fetch all members with a paid status that don't have a members_products record
|
|
10
|
+
// and have a members_product_events record with an action of "added"
|
|
11
|
+
// and fetch the product_id from the most recent record for that member
|
|
12
|
+
const memberWithTiers = await knex.select('m.id as member_id', 'mpe.product_id as product_id')
|
|
13
|
+
.from('members as m')
|
|
14
|
+
.leftJoin('members_products as mp', 'm.id', 'mp.member_id')
|
|
15
|
+
.leftJoin('members_product_events as mpe', function () {
|
|
16
|
+
this.on('m.id', 'mpe.member_id')
|
|
17
|
+
.andOn(knex.raw('mpe.created_at = (SELECT max(created_at) FROM members_product_events WHERE member_id = mpe.member_id and action = "added")'));
|
|
18
|
+
})
|
|
19
|
+
.where({'m.status': 'paid', 'mp.member_id': null, 'mpe.action': 'added'});
|
|
20
|
+
|
|
21
|
+
// create a new members_products record for each member with id, member_id and product_id
|
|
22
|
+
const toInsert = memberWithTiers.map((memberTier) => {
|
|
23
|
+
return {
|
|
24
|
+
...memberTier,
|
|
25
|
+
id: ObjectId().toHexString()
|
|
26
|
+
};
|
|
27
|
+
}).filter((memberTier) => {
|
|
28
|
+
// filter out any members that don't have a product_id for some reason
|
|
29
|
+
if (!memberTier.product_id) {
|
|
30
|
+
logging.warn(`Invalid record found - member_id: ${memberTier.member_id} is without product_id`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
logging.info(`Inserting ${toInsert.length} records into members_products`);
|
|
37
|
+
await knex.batchInsert('members_products', toInsert);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
logging.warn('Failed to restore member<>tier mapping for members with paid status');
|
|
40
|
+
logging.warn(err);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
async function down() {
|
|
44
|
+
// np-op: we don't want to delete the missing records we've just inserted
|
|
45
|
+
}
|
|
46
|
+
);
|
|
@@ -219,8 +219,8 @@ const Member = ghostBookshelf.Model.extend({
|
|
|
219
219
|
|
|
220
220
|
async updateTierExpiry(products = [], options = {}) {
|
|
221
221
|
for (const product of products) {
|
|
222
|
-
if (product?.
|
|
223
|
-
const expiry = new Date(product.expiry_at);
|
|
222
|
+
if (product?.id) {
|
|
223
|
+
const expiry = product.expiry_at ? new Date(product.expiry_at) : null;
|
|
224
224
|
const queryOptions = _.extend({}, options, {
|
|
225
225
|
query: {where: {product_id: product.id}}
|
|
226
226
|
});
|
|
@@ -93,7 +93,7 @@ module.exports = {
|
|
|
93
93
|
} catch (error) {
|
|
94
94
|
return new FailedBatch(emailBatchId, error);
|
|
95
95
|
}
|
|
96
|
-
}, {concurrency:
|
|
96
|
+
}, {concurrency: 2});
|
|
97
97
|
|
|
98
98
|
const successes = batchResults.filter(response => (response instanceof SuccessfulBatch));
|
|
99
99
|
const failures = batchResults.filter(response => (response instanceof FailedBatch));
|
|
@@ -196,8 +196,11 @@ module.exports = {
|
|
|
196
196
|
|
|
197
197
|
// log any error that didn't come from the provider which would have already logged it
|
|
198
198
|
if (!error.code || error.code !== 'BULK_EMAIL_SEND_FAILED') {
|
|
199
|
-
let ghostError = new errors.
|
|
200
|
-
err: error
|
|
199
|
+
let ghostError = new errors.EmailError({
|
|
200
|
+
err: error,
|
|
201
|
+
code: 'BULK_EMAIL_SEND_FAILED',
|
|
202
|
+
message: `Error sending email batch ${emailBatchId}`,
|
|
203
|
+
context: error.message
|
|
201
204
|
});
|
|
202
205
|
sentry.captureException(ghostError);
|
|
203
206
|
logging.error(ghostError);
|
|
@@ -274,7 +277,7 @@ module.exports = {
|
|
|
274
277
|
});
|
|
275
278
|
|
|
276
279
|
sentry.captureException(ghostError);
|
|
277
|
-
logging.
|
|
280
|
+
logging.error(ghostError);
|
|
278
281
|
|
|
279
282
|
debug(`failed to send message (${Date.now() - startTime}ms)`);
|
|
280
283
|
throw ghostError;
|
|
@@ -7,7 +7,7 @@ const jobsService = require('../../jobs');
|
|
|
7
7
|
let hasScheduled = false;
|
|
8
8
|
|
|
9
9
|
module.exports = {
|
|
10
|
-
async scheduleRecurringJobs() {
|
|
10
|
+
async scheduleRecurringJobs(skipEmailCheck = false) {
|
|
11
11
|
if (
|
|
12
12
|
!hasScheduled &&
|
|
13
13
|
config.get('emailAnalytics') &&
|
|
@@ -17,10 +17,10 @@ module.exports = {
|
|
|
17
17
|
// Don't register email analytics job if we have no emails,
|
|
18
18
|
// processor usage from many sites spinning up threads can be high.
|
|
19
19
|
// Mega service will re-run this scheduling task when an email is sent
|
|
20
|
-
const emailCount = await models.Email
|
|
20
|
+
const emailCount = skipEmailCheck ? 1 : (await models.Email
|
|
21
21
|
.where('created_at', '>', moment.utc().subtract(30, 'days').toDate())
|
|
22
22
|
.where('status', '<>', 'failed')
|
|
23
|
-
.count();
|
|
23
|
+
.count());
|
|
24
24
|
|
|
25
25
|
if (emailCount > 0) {
|
|
26
26
|
// use a random seconds value to avoid spikes to external APIs on the minute
|
|
@@ -35,6 +35,7 @@ class EmailServiceWrapper {
|
|
|
35
35
|
const linkTracking = require('../link-tracking');
|
|
36
36
|
const audienceFeedback = require('../audience-feedback');
|
|
37
37
|
const storageUtils = require('../../adapters/storage/utils');
|
|
38
|
+
const emailAnalyticsJobs = require('../email-analytics/jobs');
|
|
38
39
|
|
|
39
40
|
// capture errors from mailgun client and log them in sentry
|
|
40
41
|
const errorHandler = (error) => {
|
|
@@ -88,7 +89,8 @@ class EmailServiceWrapper {
|
|
|
88
89
|
jobsService,
|
|
89
90
|
emailSegmenter,
|
|
90
91
|
emailRenderer,
|
|
91
|
-
db
|
|
92
|
+
db,
|
|
93
|
+
sentry
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
this.service = new EmailService({
|
|
@@ -102,7 +104,8 @@ class EmailServiceWrapper {
|
|
|
102
104
|
emailSegmenter,
|
|
103
105
|
limitService,
|
|
104
106
|
membersRepository,
|
|
105
|
-
verificationTrigger: membersService.verificationTrigger
|
|
107
|
+
verificationTrigger: membersService.verificationTrigger,
|
|
108
|
+
emailAnalyticsJobs
|
|
106
109
|
});
|
|
107
110
|
|
|
108
111
|
this.controller = new EmailController(this.service, {
|
|
@@ -21,12 +21,17 @@ module.exports = class BookshelfMentionRepository {
|
|
|
21
21
|
/** @type {Object} */
|
|
22
22
|
#MentionModel;
|
|
23
23
|
|
|
24
|
+
/** @type {import('@tryghost/domain-events')} */
|
|
25
|
+
#DomainEvents;
|
|
26
|
+
|
|
24
27
|
/**
|
|
25
28
|
* @param {object} deps
|
|
26
29
|
* @param {object} deps.MentionModel Bookshelf Model
|
|
30
|
+
* @param {import('@tryghost/domain-events')} deps.DomainEvents
|
|
27
31
|
*/
|
|
28
32
|
constructor(deps) {
|
|
29
33
|
this.#MentionModel = deps.MentionModel;
|
|
34
|
+
this.#DomainEvents = deps.DomainEvents;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
#modelToMention(model) {
|
|
@@ -113,5 +118,8 @@ module.exports = class BookshelfMentionRepository {
|
|
|
113
118
|
id: data.id
|
|
114
119
|
});
|
|
115
120
|
}
|
|
121
|
+
for (const event of mention.events) {
|
|
122
|
+
this.#DomainEvents.dispatch(event);
|
|
123
|
+
}
|
|
116
124
|
}
|
|
117
125
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const ObjectID = require('bson-objectid').default;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('@tryghost/webmentions/lib/MentionsAPI').IResourceService} IResourceService
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @implements {IResourceService}
|
|
9
|
+
*/
|
|
10
|
+
module.exports = class ResourceService {
|
|
11
|
+
/** @type {import('@tryghost/url-utils/lib/url-utils')} */
|
|
12
|
+
#urlUtils;
|
|
13
|
+
|
|
14
|
+
/** @type {import('../url')} */
|
|
15
|
+
#urlService;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} deps
|
|
19
|
+
* @param {import('@tryghost/url-utils/lib/url-utils')} deps.urlUtils
|
|
20
|
+
* @param {import('../url')} deps.urlService
|
|
21
|
+
*/
|
|
22
|
+
constructor(deps) {
|
|
23
|
+
this.#urlUtils = deps.urlUtils;
|
|
24
|
+
this.#urlService = deps.urlService;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {URL} url
|
|
29
|
+
* @returns {Promise<import('@tryghost/webmentions/lib/MentionsAPI').ResourceResult>}
|
|
30
|
+
*/
|
|
31
|
+
async getByURL(url) {
|
|
32
|
+
const path = this.#urlUtils.absoluteToRelative(url.href, {withoutSubdirectory: true});
|
|
33
|
+
const resource = this.#urlService.getResource(path);
|
|
34
|
+
if (resource?.config?.type === 'posts') {
|
|
35
|
+
return {
|
|
36
|
+
type: 'post',
|
|
37
|
+
id: ObjectID.createFromHexString(resource.data.id)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
type: null,
|
|
42
|
+
id: null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('@tryghost/webmentions/lib/MentionsAPI').IRoutingService} IRoutingService
|
|
5
|
+
* @typedef {import('@tryghost/webmentions/lib/MentionsAPI').IResourceService} IResourceService
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} IUrlUtils
|
|
10
|
+
* @prop {() => string} getSiteUrl
|
|
11
|
+
* @prop {() => string} getSubdir
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @implements {IRoutingService}
|
|
16
|
+
*/
|
|
17
|
+
module.exports = class RoutingService {
|
|
18
|
+
/** @typedef {URL} */
|
|
19
|
+
#siteUrl;
|
|
20
|
+
|
|
21
|
+
/** @typedef {IResourceService} */
|
|
22
|
+
#resourceService;
|
|
23
|
+
|
|
24
|
+
/** @typedef {import('got')} */
|
|
25
|
+
#externalRequest;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} deps
|
|
29
|
+
* @param {URL} deps.siteUrl
|
|
30
|
+
* @param {IResourceService} deps.resourceService
|
|
31
|
+
* @param {import('got')} deps.externalRequest;
|
|
32
|
+
*/
|
|
33
|
+
constructor(deps) {
|
|
34
|
+
this.#siteUrl = deps.siteUrl;
|
|
35
|
+
this.#resourceService = deps.resourceService;
|
|
36
|
+
this.#externalRequest = deps.externalRequest;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {URL} url
|
|
41
|
+
*/
|
|
42
|
+
async pageExists(url) {
|
|
43
|
+
if (this.#siteUrl.origin !== url.origin) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const subdir = removeTrailingSlash(this.#siteUrl.pathname);
|
|
47
|
+
if (subdir && !url.pathname.startsWith(subdir)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const resource = await this.#resourceService.getByURL(url);
|
|
52
|
+
|
|
53
|
+
if (resource?.type !== null) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const response = await this.#externalRequest.head(url, {
|
|
59
|
+
followRedirect: false
|
|
60
|
+
});
|
|
61
|
+
if (response.statusCode < 400 && response.statusCode > 199) {
|
|
62
|
+
return true;
|
|
63
|
+
} else {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logging.error(err);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} str
|
|
75
|
+
*/
|
|
76
|
+
function removeTrailingSlash(str) {
|
|
77
|
+
return str.replace(/\/$/, '');
|
|
78
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const ObjectID = require('bson-objectid').default;
|
|
2
1
|
const MentionController = require('./MentionController');
|
|
3
2
|
const WebmentionMetadata = require('./WebmentionMetadata');
|
|
4
3
|
const {
|
|
@@ -7,6 +6,8 @@ const {
|
|
|
7
6
|
MentionDiscoveryService
|
|
8
7
|
} = require('@tryghost/webmentions');
|
|
9
8
|
const BookshelfMentionRepository = require('./BookshelfMentionRepository');
|
|
9
|
+
const ResourceService = require('./ResourceService');
|
|
10
|
+
const RoutingService = require('./RoutingService');
|
|
10
11
|
const models = require('../../models');
|
|
11
12
|
const events = require('../../lib/common/events');
|
|
12
13
|
const externalRequest = require('../../../server/lib/request-external.js');
|
|
@@ -14,53 +15,39 @@ const urlUtils = require('../../../shared/url-utils');
|
|
|
14
15
|
const outputSerializerUrlUtil = require('../../../server/api/endpoints/utils/serializers/output/utils/url');
|
|
15
16
|
const labs = require('../../../shared/labs');
|
|
16
17
|
const urlService = require('../url');
|
|
18
|
+
const DomainEvents = require('@tryghost/domain-events');
|
|
17
19
|
|
|
18
20
|
function getPostUrl(post) {
|
|
19
21
|
const jsonModel = {};
|
|
20
22
|
outputSerializerUrlUtil.forPost(post.id, jsonModel, {options: {}});
|
|
21
23
|
return jsonModel.url;
|
|
22
24
|
}
|
|
25
|
+
|
|
23
26
|
module.exports = {
|
|
24
27
|
controller: new MentionController(),
|
|
25
28
|
async init() {
|
|
26
29
|
const repository = new BookshelfMentionRepository({
|
|
27
|
-
MentionModel: models.Mention
|
|
30
|
+
MentionModel: models.Mention,
|
|
31
|
+
DomainEvents
|
|
28
32
|
});
|
|
29
33
|
const webmentionMetadata = new WebmentionMetadata();
|
|
30
34
|
const discoveryService = new MentionDiscoveryService({externalRequest});
|
|
35
|
+
const resourceService = new ResourceService({
|
|
36
|
+
urlUtils,
|
|
37
|
+
urlService
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const routingService = new RoutingService({
|
|
41
|
+
siteUrl: new URL(urlUtils.getSiteUrl()),
|
|
42
|
+
resourceService,
|
|
43
|
+
externalRequest
|
|
44
|
+
});
|
|
45
|
+
|
|
31
46
|
const api = new MentionsAPI({
|
|
32
47
|
repository,
|
|
33
48
|
webmentionMetadata,
|
|
34
|
-
resourceService
|
|
35
|
-
|
|
36
|
-
const path = urlUtils.absoluteToRelative(url.href, {withoutSubdirectory: true});
|
|
37
|
-
const resource = urlService.getResource(path);
|
|
38
|
-
if (resource?.config?.type === 'posts') {
|
|
39
|
-
return {
|
|
40
|
-
type: 'post',
|
|
41
|
-
id: ObjectID.createFromHexString(resource.data.id)
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
type: null,
|
|
46
|
-
id: null
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
routingService: {
|
|
51
|
-
async pageExists(url) {
|
|
52
|
-
const siteUrl = new URL(urlUtils.getSiteUrl());
|
|
53
|
-
if (siteUrl.origin !== url.origin) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
const subdir = urlUtils.getSubdir();
|
|
57
|
-
if (subdir && !url.pathname.startsWith(subdir)) {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
49
|
+
resourceService,
|
|
50
|
+
routingService
|
|
64
51
|
});
|
|
65
52
|
|
|
66
53
|
this.controller.init({api});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const DomainEvents = require('@tryghost/domain-events');
|
|
2
|
+
const labs = require('../../../shared/labs');
|
|
3
|
+
|
|
2
4
|
class StaffServiceWrapper {
|
|
3
5
|
init() {
|
|
4
6
|
if (this.api) {
|
|
@@ -23,7 +25,8 @@ class StaffServiceWrapper {
|
|
|
23
25
|
settingsHelpers,
|
|
24
26
|
settingsCache,
|
|
25
27
|
urlUtils,
|
|
26
|
-
DomainEvents
|
|
28
|
+
DomainEvents,
|
|
29
|
+
labs
|
|
27
30
|
});
|
|
28
31
|
|
|
29
32
|
this.api.subscribeEvents();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const debug = require('@tryghost/debug')('services:url:resources');
|
|
3
|
+
const DomainEvents = require('@tryghost/domain-events');
|
|
4
|
+
const {URLResourceUpdatedEvent} = require('@tryghost/dynamic-routing-events');
|
|
3
5
|
const Resource = require('./Resource');
|
|
4
6
|
const config = require('../../../shared/config');
|
|
5
7
|
const models = require('../../models');
|
|
@@ -272,6 +274,21 @@ class Resources {
|
|
|
272
274
|
}
|
|
273
275
|
}
|
|
274
276
|
|
|
277
|
+
/**
|
|
278
|
+
*
|
|
279
|
+
* @param {Object} model resource model
|
|
280
|
+
* @returns
|
|
281
|
+
*/
|
|
282
|
+
_containsRoutingAffectingChanges(model, ignoredProperties) {
|
|
283
|
+
if (model._changed && Object.keys(model._changed).length) {
|
|
284
|
+
return _.difference(Object.keys(model._changed), ignoredProperties).length !== 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// NOTE: returning true here as "_changed" property might not be available on attached/detached events
|
|
288
|
+
// assuming there were route affecting changes by default
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
275
292
|
/**
|
|
276
293
|
* @description Listener for "model updated" event.
|
|
277
294
|
*
|
|
@@ -297,6 +314,22 @@ class Resources {
|
|
|
297
314
|
|
|
298
315
|
const resourceConfig = _.find(this.resourcesConfig, {type: type});
|
|
299
316
|
|
|
317
|
+
// NOTE: check if any of the route-related fields were changed and only proceed if so
|
|
318
|
+
const ignoredProperties = [...resourceConfig.modelOptions.exclude, 'updated_at'];
|
|
319
|
+
if (!this._containsRoutingAffectingChanges(model, ignoredProperties)) {
|
|
320
|
+
const cachedResource = this.getByIdAndType(type, model.id);
|
|
321
|
+
|
|
322
|
+
if (cachedResource && model._changed && Object.keys(model._changed).includes('updated_at')) {
|
|
323
|
+
DomainEvents.dispatch(URLResourceUpdatedEvent.create(Object.assign(cachedResource.data, {
|
|
324
|
+
resourceType: cachedResource.config.type,
|
|
325
|
+
updated_at: model._changed.updated_at
|
|
326
|
+
})));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
debug('skipping _onResourceUpdated because only non-route-related properties changed');
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
300
333
|
// NOTE: synchronous handling for post and pages so that their URL is available without a delay
|
|
301
334
|
// for more context and future improvements check https://github.com/TryGhost/Ghost/issues/10360
|
|
302
335
|
if (['posts', 'pages'].includes(type)) {
|
|
@@ -32,6 +32,9 @@ class Urls {
|
|
|
32
32
|
/**
|
|
33
33
|
* @description Add a url to the system.
|
|
34
34
|
* @param {Object} options
|
|
35
|
+
* @param {import('./Resource')} options.resource - instance of the Resource class
|
|
36
|
+
* @param {string} options.generatorId
|
|
37
|
+
* @param {string} options.url
|
|
35
38
|
*/
|
|
36
39
|
add(options) {
|
|
37
40
|
const url = options.url;
|
|
@@ -31,7 +31,8 @@ module.exports = [
|
|
|
31
31
|
'twitter_title',
|
|
32
32
|
'twitter_description',
|
|
33
33
|
'custom_template',
|
|
34
|
-
'locale'
|
|
34
|
+
'locale',
|
|
35
|
+
'tiers'
|
|
35
36
|
],
|
|
36
37
|
withRelated: ['tags', 'authors'],
|
|
37
38
|
withRelatedPrimary: {
|
|
@@ -79,7 +80,8 @@ module.exports = [
|
|
|
79
80
|
'tags',
|
|
80
81
|
'authors',
|
|
81
82
|
'primary_tag',
|
|
82
|
-
'primary_author'
|
|
83
|
+
'primary_author',
|
|
84
|
+
'tiers'
|
|
83
85
|
],
|
|
84
86
|
filter: 'status:published+type:page'
|
|
85
87
|
},
|
|
@@ -126,7 +128,8 @@ module.exports = [
|
|
|
126
128
|
'accessibility',
|
|
127
129
|
'meta_title',
|
|
128
130
|
'meta_description',
|
|
129
|
-
'tour'
|
|
131
|
+
'tour',
|
|
132
|
+
'last_seen'
|
|
130
133
|
],
|
|
131
134
|
filter: 'visibility:public',
|
|
132
135
|
shouldHavePosts: {
|