ghost 5.115.0 → 5.115.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/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
- package/components/tryghost-adapter-manager-5.115.1.tgz +0 -0
- package/components/{tryghost-announcement-bar-settings-5.115.0.tgz → tryghost-announcement-bar-settings-5.115.1.tgz} +0 -0
- package/components/{tryghost-api-framework-5.115.0.tgz → tryghost-api-framework-5.115.1.tgz} +0 -0
- package/components/tryghost-constants-5.115.1.tgz +0 -0
- package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
- package/components/{tryghost-custom-theme-settings-service-5.115.0.tgz → tryghost-custom-theme-settings-service-5.115.1.tgz} +0 -0
- package/components/{tryghost-data-generator-5.115.0.tgz → tryghost-data-generator-5.115.1.tgz} +0 -0
- package/components/tryghost-domain-events-5.115.1.tgz +0 -0
- package/components/tryghost-donations-5.115.1.tgz +0 -0
- package/components/{tryghost-email-addresses-5.115.0.tgz → tryghost-email-addresses-5.115.1.tgz} +0 -0
- package/components/{tryghost-email-content-generator-5.115.0.tgz → tryghost-email-content-generator-5.115.1.tgz} +0 -0
- package/components/{tryghost-email-events-5.115.0.tgz → tryghost-email-events-5.115.1.tgz} +0 -0
- package/components/tryghost-email-service-5.115.1.tgz +0 -0
- package/components/{tryghost-email-suppression-list-5.115.0.tgz → tryghost-email-suppression-list-5.115.1.tgz} +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.115.0.tgz → tryghost-express-dynamic-redirects-5.115.1.tgz} +0 -0
- package/components/tryghost-ghost-5.115.1.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.115.1.tgz +0 -0
- package/components/tryghost-i18n-5.115.1.tgz +0 -0
- package/components/{tryghost-importer-handler-content-files-5.115.0.tgz → tryghost-importer-handler-content-files-5.115.1.tgz} +0 -0
- package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
- package/components/{tryghost-job-manager-5.115.0.tgz → tryghost-job-manager-5.115.1.tgz} +0 -0
- package/components/{tryghost-link-redirects-5.115.0.tgz → tryghost-link-redirects-5.115.1.tgz} +0 -0
- package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
- package/components/{tryghost-magic-link-5.115.0.tgz → tryghost-magic-link-5.115.1.tgz} +0 -0
- package/components/{tryghost-mailgun-client-5.115.0.tgz → tryghost-mailgun-client-5.115.1.tgz} +0 -0
- package/components/tryghost-member-attribution-5.115.1.tgz +0 -0
- package/components/{tryghost-member-events-5.115.0.tgz → tryghost-member-events-5.115.1.tgz} +0 -0
- package/components/{tryghost-members-api-5.115.0.tgz → tryghost-members-api-5.115.1.tgz} +0 -0
- package/components/{tryghost-members-csv-5.115.0.tgz → tryghost-members-csv-5.115.1.tgz} +0 -0
- package/components/{tryghost-members-offers-5.115.0.tgz → tryghost-members-offers-5.115.1.tgz} +0 -0
- package/components/{tryghost-members-payments-5.115.0.tgz → tryghost-members-payments-5.115.1.tgz} +0 -0
- package/components/{tryghost-milestones-5.115.0.tgz → tryghost-milestones-5.115.1.tgz} +0 -0
- package/components/{tryghost-minifier-5.115.0.tgz → tryghost-minifier-5.115.1.tgz} +0 -0
- package/components/{tryghost-mw-error-handler-5.115.0.tgz → tryghost-mw-error-handler-5.115.1.tgz} +0 -0
- package/components/{tryghost-mw-version-match-5.115.0.tgz → tryghost-mw-version-match-5.115.1.tgz} +0 -0
- package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
- package/components/tryghost-post-events-5.115.1.tgz +0 -0
- package/components/{tryghost-post-revisions-5.115.0.tgz → tryghost-post-revisions-5.115.1.tgz} +0 -0
- package/components/{tryghost-posts-service-5.115.0.tgz → tryghost-posts-service-5.115.1.tgz} +0 -0
- package/components/{tryghost-prometheus-metrics-5.115.0.tgz → tryghost-prometheus-metrics-5.115.1.tgz} +0 -0
- package/components/tryghost-recommendations-5.115.1.tgz +0 -0
- package/components/{tryghost-security-5.115.0.tgz → tryghost-security-5.115.1.tgz} +0 -0
- package/components/{tryghost-slack-notifications-5.115.0.tgz → tryghost-slack-notifications-5.115.1.tgz} +0 -0
- package/components/{tryghost-tiers-5.115.0.tgz → tryghost-tiers-5.115.1.tgz} +0 -0
- package/components/tryghost-webmentions-5.115.1.tgz +0 -0
- package/content/themes/casper/LICENSE +1 -1
- package/content/themes/casper/README.md +1 -1
- package/content/themes/source/LICENSE +1 -1
- package/content/themes/source/README.md +1 -1
- package/content/themes/source/assets/built/screen.css +1 -1
- package/content/themes/source/assets/built/screen.css.map +1 -1
- package/content/themes/source/assets/css/screen.css +11 -6
- package/content/themes/source/partials/feature-image.hbs +2 -2
- package/core/boot.js +3 -1
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +23497 -23041
- package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
- package/core/built/admin/assets/admin-x-demo/{index-0040480a.mjs → index-15df2af5.mjs} +4 -3
- package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-8ca61d78.mjs} +67 -65
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.mjs → CodeEditorView-d2e6872f.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{index-376f847c.mjs → index-8e8821e5.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f5cb3db3.mjs} +3104 -3094
- package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-e8ae4d46.mjs} +3 -3
- package/core/built/admin/assets/{chunk.524.31419fdf6fb3859ecc1e.js → chunk.524.2439684964c164c598ab.js} +6 -6
- package/core/built/admin/assets/{chunk.582.08c816d5e4ab766486a7.js → chunk.582.bf5a2bbb2c4eb69ef1e7.js} +10 -10
- package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +1 -0
- package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +1 -0
- package/core/built/admin/assets/{ghost-938b3d9c29e3564a53a22f8c8f82d351.js → ghost-df7b9558260aa27d18b195ee895b487d.js} +181 -159
- package/core/built/admin/assets/stats/stats.js +11824 -0
- package/core/built/admin/index.html +4 -4
- package/core/frontend/helpers/ghost_head.js +3 -1
- package/core/frontend/src/cards/css/cta.css +1 -1
- package/core/server/api/endpoints/slugs.js +6 -2
- package/core/server/data/importer/import-manager.js +2 -2
- package/core/server/data/importer/importers/importer-revue.js +128 -0
- package/core/server/data/importer/importers/json-to-html.js +107 -0
- package/core/server/data/migrations/utils/tables.js +2 -4
- package/core/server/lib/bootstrap-socket.js +87 -0
- package/core/server/lib/package-json/index.js +1 -0
- package/core/server/lib/package-json/package-json.js +160 -0
- package/core/server/lib/package-json/parse.js +57 -0
- package/core/server/models/base/plugins/actions.js +44 -31
- package/core/server/models/base/plugins/generate-slug.js +6 -0
- package/core/server/notify.js +1 -1
- package/core/server/services/activitypub/ActivityPubService.ts +1 -1
- package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
- package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
- package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
- package/core/server/services/api-version-compatibility/index.js +2 -2
- package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
- package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
- package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
- package/core/server/services/audience-feedback/Feedback.js +35 -0
- package/core/server/services/audience-feedback/index.js +4 -2
- package/core/server/services/auth/session/emails/signin.js +168 -0
- package/core/server/services/auth/session/index.js +2 -2
- package/core/server/services/auth/session/session-from-token.js +69 -0
- package/core/server/services/auth/session/session-service.js +364 -0
- package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
- package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
- package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
- package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
- package/core/server/services/explore-ping/ExplorePingService.js +106 -0
- package/core/server/services/explore-ping/index.js +31 -0
- package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
- package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
- package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
- package/core/server/services/invitations/accept.js +5 -2
- package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
- package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
- package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
- package/core/server/services/mail-events/MailEvent.js +20 -0
- package/core/server/services/mail-events/MailEvent.ts +10 -0
- package/core/server/services/mail-events/MailEventRepository.js +2 -0
- package/core/server/services/mail-events/MailEventRepository.ts +5 -0
- package/core/server/services/mail-events/MailEventService.js +124 -0
- package/core/server/services/mail-events/MailEventService.ts +169 -0
- package/core/server/services/mail-events/index.js +1 -1
- package/core/server/services/mail-events/libraries.d.ts +2 -0
- package/core/server/services/members/CaptchaService.js +80 -0
- package/core/server/services/members/api.js +1 -1
- package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
- package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
- package/core/server/services/members/importer/email-template.js +182 -0
- package/core/server/services/members/importer/index.js +30 -0
- package/core/server/services/members/members-ssr.js +333 -0
- package/core/server/services/members/service.js +2 -2
- package/core/server/services/posts/stats/PostStats.js +13 -0
- package/core/server/services/route-settings/SettingsPathManager.js +47 -0
- package/core/server/services/route-settings/index.js +1 -1
- package/core/server/services/stripe/README.md +63 -0
- package/core/server/services/stripe/StripeAPI.js +931 -0
- package/core/server/services/stripe/StripeMigrations.js +613 -0
- package/core/server/services/stripe/StripeService.js +175 -0
- package/core/server/services/stripe/WebhookController.js +100 -0
- package/core/server/services/stripe/WebhookManager.js +175 -0
- package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
- package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
- package/core/server/services/stripe/events/index.js +4 -0
- package/core/server/services/stripe/service.js +1 -1
- package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
- package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
- package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
- package/core/server/services/themes/loader.js +1 -1
- package/core/server/services/themes/to-json.js +1 -1
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/server/web/shared/middleware/cache-control.js +51 -0
- package/core/server/web/shared/middleware/index.js +1 -1
- package/core/server/web/well-known.js +1 -1
- package/core/shared/labs.js +3 -1
- package/core/shared/settings-cache/CacheManager.js +64 -6
- package/package.json +103 -134
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +7 -93
- package/components/tryghost-adapter-cache-redis-5.115.0.tgz +0 -0
- package/components/tryghost-adapter-manager-5.115.0.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-5.115.0.tgz +0 -0
- package/components/tryghost-audience-feedback-5.115.0.tgz +0 -0
- package/components/tryghost-bookshelf-repository-5.115.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.115.0.tgz +0 -0
- package/components/tryghost-captcha-service-5.115.0.tgz +0 -0
- package/components/tryghost-constants-5.115.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.115.0.tgz +0 -0
- package/components/tryghost-domain-events-5.115.0.tgz +0 -0
- package/components/tryghost-donations-5.115.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-5.115.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.115.0.tgz +0 -0
- package/components/tryghost-email-service-5.115.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.115.0.tgz +0 -0
- package/components/tryghost-ghost-5.115.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.115.0.tgz +0 -0
- package/components/tryghost-i18n-5.115.0.tgz +0 -0
- package/components/tryghost-identity-token-service-5.115.0.tgz +0 -0
- package/components/tryghost-importer-revue-5.115.0.tgz +0 -0
- package/components/tryghost-in-memory-repository-5.115.0.tgz +0 -0
- package/components/tryghost-link-replacer-5.115.0.tgz +0 -0
- package/components/tryghost-mail-events-5.115.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.115.0.tgz +0 -0
- package/components/tryghost-members-importer-5.115.0.tgz +0 -0
- package/components/tryghost-members-ssr-5.115.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-5.115.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-5.115.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.115.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-5.115.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-5.115.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.115.0.tgz +0 -0
- package/components/tryghost-package-json-5.115.0.tgz +0 -0
- package/components/tryghost-post-events-5.115.0.tgz +0 -0
- package/components/tryghost-recommendations-5.115.0.tgz +0 -0
- package/components/tryghost-referrers-5.115.0.tgz +0 -0
- package/components/tryghost-session-service-5.115.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-5.115.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-5.115.0.tgz +0 -0
- package/components/tryghost-webmentions-5.115.0.tgz +0 -0
- package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
- package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
|
@@ -3,9 +3,20 @@ const errors = require('@tryghost/errors');
|
|
|
3
3
|
const logging = require('@tryghost/logging');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
+
* This plugin is used to add actions to the database. It backs the "audit log" feature we have in Ghost.
|
|
7
|
+
*
|
|
8
|
+
* The functions here are triggered by the `onCreated`, `onUpdated`, `onDeleted` functions in the `events`
|
|
9
|
+
* plugin, with some extra ones for niche other events.
|
|
10
|
+
*
|
|
6
11
|
* @param {import('bookshelf')} Bookshelf
|
|
7
12
|
*/
|
|
8
13
|
module.exports = function (Bookshelf) {
|
|
14
|
+
/**
|
|
15
|
+
* Insert an action into the database
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} data - The data to insert
|
|
18
|
+
* @param {Object} options - The options object
|
|
19
|
+
*/
|
|
9
20
|
const insertAction = (data, options) => {
|
|
10
21
|
// CASE: model does not support action for target event
|
|
11
22
|
if (!data) {
|
|
@@ -39,7 +50,13 @@ module.exports = function (Bookshelf) {
|
|
|
39
50
|
}
|
|
40
51
|
};
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Add an action to the database
|
|
55
|
+
*
|
|
56
|
+
* @param {import('bookshelf').Model} model - The model to add the action to
|
|
57
|
+
* @param {string} event - The event that triggered the action
|
|
58
|
+
* @param {Object} options - The options object
|
|
59
|
+
*/
|
|
43
60
|
const addAction = (model, event, options) => {
|
|
44
61
|
if (!model.wasChanged()) {
|
|
45
62
|
return;
|
|
@@ -58,11 +75,14 @@ module.exports = function (Bookshelf) {
|
|
|
58
75
|
/**
|
|
59
76
|
* Constructs data to be stored in the database with info
|
|
60
77
|
* on particular actions
|
|
78
|
+
*
|
|
79
|
+
* @param {string} event - The event that triggered the action
|
|
80
|
+
* @param {Object} options - The options object
|
|
81
|
+
* @returns {Object} The data to be stored in the database
|
|
61
82
|
*/
|
|
62
83
|
getAction(event, options) {
|
|
84
|
+
// Ignore internal updates (`options.context.internal`) for now
|
|
63
85
|
const actor = this.getActor(options);
|
|
64
|
-
|
|
65
|
-
// @NOTE: we ignore internal updates (`options.context.internal`) for now
|
|
66
86
|
if (!actor) {
|
|
67
87
|
return;
|
|
68
88
|
}
|
|
@@ -71,12 +91,7 @@ module.exports = function (Bookshelf) {
|
|
|
71
91
|
return;
|
|
72
92
|
}
|
|
73
93
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (typeof resourceType === 'function') {
|
|
77
|
-
resourceType = resourceType.bind(this)();
|
|
78
|
-
}
|
|
79
|
-
|
|
94
|
+
const resourceType = this.actionsResourceType;
|
|
80
95
|
if (!resourceType) {
|
|
81
96
|
return;
|
|
82
97
|
}
|
|
@@ -85,12 +100,14 @@ module.exports = function (Bookshelf) {
|
|
|
85
100
|
action_name: options.actionName
|
|
86
101
|
};
|
|
87
102
|
|
|
103
|
+
// Used to attach extra content to the action (ie. the key + group for settings changes)
|
|
88
104
|
if (this.actionsExtraContext && Array.isArray(this.actionsExtraContext)) {
|
|
89
105
|
for (const c of this.actionsExtraContext) {
|
|
90
106
|
context[c] = this.get(c) || this.previous(c);
|
|
91
107
|
}
|
|
92
108
|
}
|
|
93
109
|
|
|
110
|
+
// Attach the primary name to the action (ie. the title or name of the model)
|
|
94
111
|
if (event === 'deleted') {
|
|
95
112
|
context.primary_name = (this.previous('title') || this.previous('name'));
|
|
96
113
|
} else if (['added', 'edited'].includes(event)) {
|
|
@@ -112,21 +129,17 @@ module.exports = function (Bookshelf) {
|
|
|
112
129
|
return data;
|
|
113
130
|
},
|
|
114
131
|
|
|
115
|
-
/**
|
|
116
|
-
* @NOTE:
|
|
117
|
-
*
|
|
118
|
-
* We add actions step by step and define how they should look like.
|
|
119
|
-
* Each post update triggers a couple of events, which we don't want to add actions for.
|
|
120
|
-
*
|
|
121
|
-
* e.g. transform post to page triggers a handful of events including `post.deleted` and `page.added`
|
|
122
|
-
*
|
|
123
|
-
* We protect adding too many and uncontrolled events.
|
|
124
|
-
*
|
|
125
|
-
* We could embed adding actions more nicely in the future e.g. plugin.
|
|
126
|
-
*/
|
|
127
132
|
addAction
|
|
128
133
|
}, {
|
|
129
134
|
addAction,
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Add actions for bulk actions
|
|
138
|
+
*
|
|
139
|
+
* @param {string} event - The event that triggered the action
|
|
140
|
+
* @param {number[]} ids - The ids of the models that were affected
|
|
141
|
+
* @param {Object} options - The options object
|
|
142
|
+
*/
|
|
130
143
|
async addActions(event, ids, options) {
|
|
131
144
|
if (ids.length === 1) {
|
|
132
145
|
// We want to store an event for a single model in the actions table
|
|
@@ -141,27 +154,27 @@ module.exports = function (Bookshelf) {
|
|
|
141
154
|
},
|
|
142
155
|
|
|
143
156
|
/**
|
|
144
|
-
* Constructs data to be stored in the database
|
|
145
|
-
*
|
|
157
|
+
* Constructs data for bulk actions to be stored in the database
|
|
158
|
+
*
|
|
159
|
+
* @param {string} event - The event that triggered the action
|
|
160
|
+
* @param {number} count - The number of models that were affected
|
|
161
|
+
* @param {Object} options - The options object
|
|
162
|
+
* @returns {Object} The data to be stored in the database
|
|
146
163
|
*/
|
|
147
164
|
getBulkAction(event, count, options) {
|
|
165
|
+
// Ignore internal updates (`options.context.internal`) for now
|
|
148
166
|
const actor = this.prototype.getActor(options);
|
|
149
|
-
|
|
150
|
-
// @NOTE: we ignore internal updates (`options.context.internal`) for now
|
|
151
167
|
if (!actor) {
|
|
152
168
|
return;
|
|
153
169
|
}
|
|
154
170
|
|
|
171
|
+
// Models can opt-in to their CRUD actions being collected (we do this so we don't
|
|
172
|
+
// log every single action)
|
|
155
173
|
if (!this.prototype.actionsCollectCRUD) {
|
|
156
174
|
return;
|
|
157
175
|
}
|
|
158
176
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (typeof resourceType === 'function') {
|
|
162
|
-
resourceType = resourceType.bind(this)();
|
|
163
|
-
}
|
|
164
|
-
|
|
177
|
+
const resourceType = this.prototype.actionsResourceType;
|
|
165
178
|
if (!resourceType) {
|
|
166
179
|
return;
|
|
167
180
|
}
|
|
@@ -39,6 +39,12 @@ module.exports = function (Bookshelf) {
|
|
|
39
39
|
return slugToFind;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
if (options.modelId) {
|
|
43
|
+
if (found.id === options.modelId) {
|
|
44
|
+
return slugToFind;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
slugTryCount += 1;
|
|
43
49
|
|
|
44
50
|
// If we shortened, go back to the full version and try again
|
package/core/server/notify.js
CHANGED
|
@@ -51,7 +51,7 @@ async function notify(type, error = null) {
|
|
|
51
51
|
// CASE: use bootstrap socket to communicate with CLI for systemd
|
|
52
52
|
let socketAddress = config.get('bootstrap-socket');
|
|
53
53
|
if (socketAddress) {
|
|
54
|
-
const bootstrapSocket = require('
|
|
54
|
+
const bootstrapSocket = require('./lib/bootstrap-socket');
|
|
55
55
|
return bootstrapSocket.connectAndSend(socketAddress, message);
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import ObjectID from 'bson-objectid';
|
|
2
2
|
import {Knex} from 'knex';
|
|
3
|
-
import {IdentityTokenService} from '
|
|
3
|
+
import {IdentityTokenService} from '../identity-tokens/IdentityTokenService';
|
|
4
4
|
import fetch from 'node-fetch';
|
|
5
5
|
|
|
6
6
|
type ExpectedWebhook = {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const VersionNotificationsDataService = require('./VersionNotificationsDataService');
|
|
3
|
+
const EmailContentGenerator = require('@tryghost/email-content-generator');
|
|
4
|
+
|
|
5
|
+
class APIVersionCompatibilityService {
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} options
|
|
9
|
+
* @param {Object} options.UserModel - ghost user model
|
|
10
|
+
* @param {Object} options.ApiKeyModel - ghost api key model
|
|
11
|
+
* @param {Object} options.settingsService - ghost settings service
|
|
12
|
+
* @param {(Object: {subject: String, to: String, text: String, html: String}) => Promise<any>} options.sendEmail - email sending function
|
|
13
|
+
* @param {Function} options.getSiteUrl
|
|
14
|
+
* @param {Function} options.getSiteTitle
|
|
15
|
+
*/
|
|
16
|
+
constructor({UserModel, ApiKeyModel, settingsService, sendEmail, getSiteUrl, getSiteTitle}) {
|
|
17
|
+
this.sendEmail = sendEmail;
|
|
18
|
+
|
|
19
|
+
this.versionNotificationsDataService = new VersionNotificationsDataService({
|
|
20
|
+
UserModel,
|
|
21
|
+
ApiKeyModel,
|
|
22
|
+
settingsService
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
this.emailContentGenerator = new EmailContentGenerator({
|
|
26
|
+
getSiteUrl,
|
|
27
|
+
getSiteTitle,
|
|
28
|
+
templatesDir: path.join(__dirname, 'templates')
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Version mismatch handler doing the logic of picking a template and sending a notification email
|
|
34
|
+
* @param {Object} options
|
|
35
|
+
* @param {string} options.acceptVersion - client's accept-version header value
|
|
36
|
+
* @param {string} options.contentVersion - server's content-version header value
|
|
37
|
+
* @param {string} options.apiKeyValue - key value (secret for Content API and kid for Admin API) used to access the API
|
|
38
|
+
* @param {string} options.apiKeyType - key type used to access the API
|
|
39
|
+
* @param {string} options.requestURL - url that was requested and failed compatibility test
|
|
40
|
+
* @param {string} [options.userAgent] - client's user-agent header value
|
|
41
|
+
*/
|
|
42
|
+
async handleMismatch({acceptVersion, contentVersion, apiKeyValue, apiKeyType, requestURL, userAgent = ''}) {
|
|
43
|
+
if (!await this.versionNotificationsDataService.fetchNotification(acceptVersion)) {
|
|
44
|
+
const integration = await this.versionNotificationsDataService.getIntegration(apiKeyValue, apiKeyType);
|
|
45
|
+
|
|
46
|
+
// We couldn't find the integration
|
|
47
|
+
if (!integration) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const {
|
|
52
|
+
name: integrationName,
|
|
53
|
+
type: integrationType
|
|
54
|
+
} = integration;
|
|
55
|
+
|
|
56
|
+
// @NOTE: "internal" or "core" integrations (https://ghost.notion.site/Data-Types-e5dc54dd0078443f9afd6b2abda443c4)
|
|
57
|
+
// are maintained by Ghost team, so there is no sense notifying the instance owner about it's incompatibility.
|
|
58
|
+
// The other two integration types: "builtin" and "custom", is when we want to notify about incompatibility.
|
|
59
|
+
if (['internal', 'core'].includes(integrationType)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const trimmedUseAgent = userAgent.split('/')[0];
|
|
64
|
+
const emails = await this.versionNotificationsDataService.getNotificationEmails();
|
|
65
|
+
|
|
66
|
+
for (const email of emails) {
|
|
67
|
+
const template = (trimmedUseAgent === 'Zapier')
|
|
68
|
+
? 'zapier-mismatch'
|
|
69
|
+
: 'generic-mismatch';
|
|
70
|
+
|
|
71
|
+
const subject = (trimmedUseAgent === 'Zapier')
|
|
72
|
+
? 'Attention required: One of your Zaps has failed'
|
|
73
|
+
: `Attention required: Your ${integrationName} integration has failed`;
|
|
74
|
+
|
|
75
|
+
const {html, text} = await this.emailContentGenerator.getContent({
|
|
76
|
+
template,
|
|
77
|
+
data: {
|
|
78
|
+
acceptVersion,
|
|
79
|
+
contentVersion,
|
|
80
|
+
clientName: integrationName,
|
|
81
|
+
recipientEmail: email,
|
|
82
|
+
requestURL: requestURL
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await this.sendEmail({
|
|
87
|
+
subject,
|
|
88
|
+
to: email,
|
|
89
|
+
html,
|
|
90
|
+
text
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await this.versionNotificationsDataService.saveNotification(acceptVersion);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = APIVersionCompatibilityService;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const internalContext = {
|
|
2
|
+
internal: true
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
class VersionNotificationsDataService {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} options
|
|
8
|
+
* @param {Object} options.UserModel - ghost user model
|
|
9
|
+
* @param {Object} options.ApiKeyModel - ghost api key model
|
|
10
|
+
* @param {Object} options.settingsService - ghost settings service
|
|
11
|
+
*/
|
|
12
|
+
constructor({UserModel, ApiKeyModel, settingsService}) {
|
|
13
|
+
this.UserModel = UserModel;
|
|
14
|
+
this.ApiKeyModel = ApiKeyModel;
|
|
15
|
+
this.settingsService = settingsService;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async fetchNotification(acceptVersion) {
|
|
19
|
+
const setting = await this.settingsService.read('version_notifications', internalContext);
|
|
20
|
+
const versionNotifications = JSON.parse(setting.version_notifications.value);
|
|
21
|
+
|
|
22
|
+
return versionNotifications.find(version => version === acceptVersion);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async saveNotification(acceptVersion) {
|
|
26
|
+
const setting = await this.settingsService.read('version_notifications', internalContext);
|
|
27
|
+
const versionNotifications = JSON.parse(setting.version_notifications.value);
|
|
28
|
+
|
|
29
|
+
if (!versionNotifications.find(version => version === acceptVersion)) {
|
|
30
|
+
versionNotifications.push(acceptVersion);
|
|
31
|
+
|
|
32
|
+
return this.settingsService.edit([{
|
|
33
|
+
key: 'version_notifications',
|
|
34
|
+
value: JSON.stringify(versionNotifications)
|
|
35
|
+
}], {
|
|
36
|
+
context: internalContext
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getNotificationEmails() {
|
|
42
|
+
const data = await this.UserModel.findAll(Object.assign({
|
|
43
|
+
withRelated: ['roles'],
|
|
44
|
+
filter: 'status:active'
|
|
45
|
+
}, internalContext));
|
|
46
|
+
|
|
47
|
+
const adminEmails = data
|
|
48
|
+
.toJSON()
|
|
49
|
+
.filter(user => ['Owner', 'Administrator'].includes(user.roles[0].name))
|
|
50
|
+
.map(user => user.email);
|
|
51
|
+
|
|
52
|
+
return adminEmails;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* This method is for internal use only.
|
|
57
|
+
*
|
|
58
|
+
* @param {String} key - api key identification value, it's "secret" in case of Content API key and "id" for Admin API
|
|
59
|
+
* @param {String} type - one of "content" or "admin" values
|
|
60
|
+
* @returns {Promise<Object | null>} Integration JSON object
|
|
61
|
+
*/
|
|
62
|
+
async getIntegration(key, type) {
|
|
63
|
+
let queryOptions = null;
|
|
64
|
+
|
|
65
|
+
if (type === 'content') {
|
|
66
|
+
queryOptions = {secret: key};
|
|
67
|
+
} else if (type === 'admin') {
|
|
68
|
+
queryOptions = {id: key};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const apiKey = await this.ApiKeyModel.findOne(queryOptions, {withRelated: ['integration']});
|
|
72
|
+
if (!apiKey) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return apiKey.relations.integration.toJSON();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = VersionNotificationsDataService;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Remove 'Ghost' from raw authorization header and extract the JWT token.
|
|
5
|
+
* Eg. Authorization: Ghost ${JWT}
|
|
6
|
+
* @param {string} header
|
|
7
|
+
*/
|
|
8
|
+
const extractTokenFromHeader = (header) => {
|
|
9
|
+
const [scheme, token] = header.split(' ');
|
|
10
|
+
|
|
11
|
+
if (/^Ghost$/i.test(scheme)) {
|
|
12
|
+
return token;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const extractAdminAPIKey = (token) => {
|
|
17
|
+
const decoded = jwt.decode(token, {complete: true});
|
|
18
|
+
|
|
19
|
+
if (!decoded || !decoded.header || !decoded.header.kid) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return decoded.header.kid;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} ApiKey
|
|
28
|
+
* @prop {string} key
|
|
29
|
+
* @prop {string} type
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* When it's a Content API the function resolves with the value of the key secret.
|
|
34
|
+
* When it's an Admin API the function resolves with the value of the key id.
|
|
35
|
+
*
|
|
36
|
+
* @param {import('express').Request} req
|
|
37
|
+
* @returns {ApiKey}
|
|
38
|
+
*/
|
|
39
|
+
const extractAPIKey = (req) => {
|
|
40
|
+
let keyValue = null;
|
|
41
|
+
let keyType = null;
|
|
42
|
+
|
|
43
|
+
if (req.query && req.query.key) {
|
|
44
|
+
keyValue = req.query.key;
|
|
45
|
+
keyType = 'content';
|
|
46
|
+
} else if (req.headers && req.headers.authorization) {
|
|
47
|
+
keyValue = extractAdminAPIKey(extractTokenFromHeader(req.headers.authorization));
|
|
48
|
+
keyType = 'admin';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
key: keyValue,
|
|
53
|
+
type: keyType
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
module.exports = extractAPIKey;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const APIVersionCompatibilityService = require('
|
|
2
|
-
const versionMismatchHandler = require('
|
|
1
|
+
const APIVersionCompatibilityService = require('./APIVersionCompatibilityService');
|
|
2
|
+
const versionMismatchHandler = require('./mw-api-version-mismatch');
|
|
3
3
|
const ghostVersion = require('@tryghost/version');
|
|
4
4
|
const {GhostMailer} = require('../mail');
|
|
5
5
|
const settingsService = require('../settings/settings-service');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const extractApiKey = require('./extract-api-key');
|
|
2
|
+
|
|
3
|
+
const versionMismatchHandler = (APIVersionCompatibilityService) => {
|
|
4
|
+
/**
|
|
5
|
+
* @param {Object} err
|
|
6
|
+
* @param {import('express').Request} req
|
|
7
|
+
* @param {import('express').Response} res
|
|
8
|
+
* @param {import('express').NextFunction} next
|
|
9
|
+
*/
|
|
10
|
+
return async function versionMismatchHandlerMiddleware(err, req, res, next) {
|
|
11
|
+
if (err && err.errorType === 'RequestNotAcceptableError') {
|
|
12
|
+
if (err.code === 'UPDATE_CLIENT') {
|
|
13
|
+
const {key, type} = extractApiKey(req);
|
|
14
|
+
const requestURL = req.originalUrl.split('?').shift();
|
|
15
|
+
|
|
16
|
+
await APIVersionCompatibilityService.handleMismatch({
|
|
17
|
+
acceptVersion: req.headers['accept-version'],
|
|
18
|
+
contentVersion: `v${res.locals.safeVersion}`,
|
|
19
|
+
requestURL,
|
|
20
|
+
userAgent: req.headers['user-agent'],
|
|
21
|
+
apiKeyValue: key,
|
|
22
|
+
apiKeyType: type
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
next(err);
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = versionMismatchHandler;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const Feedback = require('./Feedback');
|
|
2
|
+
const errors = require('@tryghost/errors');
|
|
3
|
+
const tpl = require('@tryghost/tpl');
|
|
4
|
+
|
|
5
|
+
const messages = {
|
|
6
|
+
invalidScore: 'Invalid feedback score. Only 1 or 0 is currently allowed.',
|
|
7
|
+
postNotFound: 'Post not found.',
|
|
8
|
+
memberNotFound: 'Member not found.'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} IFeedbackRepository
|
|
13
|
+
* @prop {(feedback: Feedback) => Promise<void>} add
|
|
14
|
+
* @prop {(feedback: Feedback) => Promise<void>} edit
|
|
15
|
+
* @prop {(postId, memberId) => Promise<Feedback>} get
|
|
16
|
+
* @prop {(id: string) => Promise<Post|undefined>} getPostById
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
class AudienceFeedbackController {
|
|
20
|
+
/** @type IFeedbackRepository */
|
|
21
|
+
#repository;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} deps
|
|
25
|
+
* @param {IFeedbackRepository} deps.repository
|
|
26
|
+
*/
|
|
27
|
+
constructor(deps) {
|
|
28
|
+
this.#repository = deps.repository;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get member from frame
|
|
33
|
+
*/
|
|
34
|
+
#getMember(frame) {
|
|
35
|
+
if (!frame.options?.context?.member?.id) {
|
|
36
|
+
// This is an internal server error because authentication should happen outside this service.
|
|
37
|
+
throw new errors.InternalServerError({
|
|
38
|
+
message: tpl(messages.memberNotFound)
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return frame.options.context.member;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async add(frame) {
|
|
45
|
+
const data = frame.data.feedback[0];
|
|
46
|
+
const postId = data.post_id;
|
|
47
|
+
const score = data.score;
|
|
48
|
+
|
|
49
|
+
if (![0, 1].includes(score)) {
|
|
50
|
+
throw new errors.ValidationError({
|
|
51
|
+
message: tpl(messages.invalidScore)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const member = this.#getMember(frame);
|
|
56
|
+
|
|
57
|
+
const post = await this.#repository.getPostById(postId);
|
|
58
|
+
if (!post) {
|
|
59
|
+
throw new errors.NotFoundError({
|
|
60
|
+
message: tpl(messages.postNotFound)
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const existing = await this.#repository.get(post.id, member.id);
|
|
65
|
+
if (existing) {
|
|
66
|
+
if (existing.score === score) {
|
|
67
|
+
// Don't save so we don't update the updated_at timestamp
|
|
68
|
+
return existing;
|
|
69
|
+
}
|
|
70
|
+
existing.score = score;
|
|
71
|
+
await this.#repository.edit(existing);
|
|
72
|
+
return existing;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const feedback = new Feedback({
|
|
76
|
+
memberId: member.id,
|
|
77
|
+
postId: post.id,
|
|
78
|
+
score
|
|
79
|
+
});
|
|
80
|
+
await this.#repository.add(feedback);
|
|
81
|
+
return feedback;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = AudienceFeedbackController;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class AudienceFeedbackService {
|
|
2
|
+
/** @type URL */
|
|
3
|
+
#baseURL;
|
|
4
|
+
/** @type {Object} */
|
|
5
|
+
#urlService;
|
|
6
|
+
/**
|
|
7
|
+
* @param {object} deps
|
|
8
|
+
* @param {object} deps.config
|
|
9
|
+
* @param {URL} deps.config.baseURL
|
|
10
|
+
* @param {object} deps.urlService
|
|
11
|
+
*/
|
|
12
|
+
constructor(deps) {
|
|
13
|
+
this.#baseURL = deps.config.baseURL;
|
|
14
|
+
this.#urlService = deps.urlService;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} uuid
|
|
18
|
+
* @param {string} postId
|
|
19
|
+
* @param {0 | 1} score
|
|
20
|
+
* @param {string} key - hashed uuid value
|
|
21
|
+
*/
|
|
22
|
+
buildLink(uuid, postId, score, key) {
|
|
23
|
+
let postUrl = this.#urlService.getUrlByResourceId(postId, {absolute: true});
|
|
24
|
+
|
|
25
|
+
if (postUrl.match(/\/404\//)) {
|
|
26
|
+
postUrl = this.#baseURL;
|
|
27
|
+
}
|
|
28
|
+
const url = new URL(postUrl);
|
|
29
|
+
url.hash = `#/feedback/${postId}/${score}/?uuid=${encodeURIComponent(uuid)}&key=${encodeURIComponent(key)}`;
|
|
30
|
+
return url;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = AudienceFeedbackService;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const ObjectID = require('bson-objectid').default;
|
|
2
|
+
|
|
3
|
+
module.exports = class Feedback {
|
|
4
|
+
/** @type {ObjectID} */
|
|
5
|
+
id;
|
|
6
|
+
/** @type {number} */
|
|
7
|
+
score;
|
|
8
|
+
/** @type {ObjectID} */
|
|
9
|
+
memberId;
|
|
10
|
+
/** @type {ObjectID} */
|
|
11
|
+
postId;
|
|
12
|
+
|
|
13
|
+
constructor(data) {
|
|
14
|
+
if (!data.id) {
|
|
15
|
+
this.id = new ObjectID();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof data.id === 'string') {
|
|
19
|
+
this.id = ObjectID.createFromHexString(data.id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.score = data.score ?? 0;
|
|
23
|
+
if (typeof data.memberId === 'string') {
|
|
24
|
+
this.memberId = ObjectID.createFromHexString(data.memberId);
|
|
25
|
+
} else {
|
|
26
|
+
this.memberId = data.memberId;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof data.postId === 'string') {
|
|
30
|
+
this.postId = ObjectID.createFromHexString(data.postId);
|
|
31
|
+
} else {
|
|
32
|
+
this.postId = data.postId;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const urlUtils = require('../../../shared/url-utils');
|
|
2
2
|
const urlService = require('../../services/url');
|
|
3
|
+
|
|
4
|
+
const AudienceFeedbackService = require('./AudienceFeedbackService');
|
|
5
|
+
const AudienceFeedbackController = require('./AudienceFeedbackController');
|
|
6
|
+
const Feedback = require('./Feedback');
|
|
3
7
|
const FeedbackRepository = require('./FeedbackRepository');
|
|
4
8
|
|
|
5
9
|
class AudienceFeedbackServiceWrapper {
|
|
@@ -12,8 +16,6 @@ class AudienceFeedbackServiceWrapper {
|
|
|
12
16
|
// Wire up all the dependencies
|
|
13
17
|
const models = require('../../models');
|
|
14
18
|
|
|
15
|
-
const {AudienceFeedbackService, AudienceFeedbackController, Feedback} = require('@tryghost/audience-feedback');
|
|
16
|
-
|
|
17
19
|
this.repository = new FeedbackRepository({
|
|
18
20
|
Member: models.Member,
|
|
19
21
|
MemberFeedback: models.MemberFeedback,
|