ghost 5.9.4 → 5.10.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-api-framework-0.0.0.tgz +0 -0
- package/components/tryghost-domain-events-0.0.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
- package/components/tryghost-email-content-generator-0.0.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
- package/components/tryghost-job-manager-0.0.0.tgz +0 -0
- package/components/tryghost-magic-link-0.0.0.tgz +0 -0
- package/components/tryghost-mailgun-client-0.0.0.tgz +0 -0
- package/components/tryghost-member-attribution-0.0.0.tgz +0 -0
- package/components/tryghost-member-events-0.0.0.tgz +0 -0
- package/components/tryghost-members-api-0.0.0.tgz +0 -0
- package/components/tryghost-members-events-service-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-ssr-0.0.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-0.0.0.tgz +0 -0
- package/components/tryghost-oembed-service-0.0.0.tgz +0 -0
- package/components/tryghost-security-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/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-0.0.0.tgz +0 -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 +8 -5
- package/content/themes/casper/package.json +1 -1
- package/core/boot.js +2 -0
- package/core/bridge.js +2 -0
- package/core/built/admin/assets/{chunk.143.1c158e8ef19f10e5439c.js → chunk.143.6a3c46a89c731b86a730.js} +6 -6
- package/core/built/admin/assets/{chunk.174.eec7f6398cef4c3e2485.js → chunk.174.0364e8abdae8210d8e6d.js} +31 -29
- package/core/built/admin/assets/{chunk.178.506264293194a4922091.js → chunk.178.8a19c35ce1a7cf4249ce.js} +4 -4
- package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js → chunk.351.ea4a4ff4b40d5f2ad141.js} +22 -19
- package/core/built/admin/assets/{chunk.351.73f27952f867334a8228.js.LICENSE.txt → chunk.351.ea4a4ff4b40d5f2ad141.js.LICENSE.txt} +0 -0
- package/core/built/admin/assets/{ghost-facfdf4a7d9759c5b681340805f21fd8.css → ghost-13baab17b3f54b21f341fb8f36f83110.css} +1 -1
- package/core/built/admin/assets/{ghost-b441c9cfa2e31453e86460e50ae7e378.js → ghost-ced03a7ac75c3148e0ea7d1bf51e39fc.js} +319 -282
- package/core/built/admin/assets/{ghost-dark-4080c8f100997d4b8947f5da0e7946a1.css → ghost-dark-b0500577a42e2770994e6aef0e70f182.css} +1 -1
- package/core/built/admin/assets/icons/ghost-orb-pink.svg +10 -0
- package/core/built/admin/assets/img/logos/orb-pink-3-a2c52eb9fda9f2401ea706c3f24976ff.png +0 -0
- package/core/built/admin/assets/{vendor-516c9e43b4aeb92079dc1ab92c9ce492.js → vendor-a1ae7a38d5c38fcba5609eed4e37f02a.js} +73 -70
- package/core/built/admin/index.html +6 -6
- package/core/frontend/helpers/ghost_head.js +4 -0
- package/core/frontend/helpers/search.js +42 -0
- package/core/frontend/services/member-attribution-assets/index.js +4 -0
- package/core/frontend/services/member-attribution-assets/service.js +83 -0
- package/core/frontend/src/member-attribution/.eslintrc +10 -0
- package/core/frontend/src/member-attribution/member-attribution.js +90 -0
- package/core/frontend/web/site.js +3 -0
- package/core/server/adapters/cache/ImageSizesCacheSyncInMemory.js +7 -0
- package/core/server/adapters/cache/SettingsCacheSyncInMemory.js +7 -0
- package/core/server/api/endpoints/posts.js +10 -1
- package/core/server/api/endpoints/utils/serializers/input/posts.js +1 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +5 -0
- package/core/server/data/exporter/table-lists.js +2 -0
- package/core/server/data/migrations/versions/5.10/2022-08-15-05-34-add-expiry-at-column-to-members-products.js +6 -0
- package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-member-created-events-table.js +11 -0
- package/core/server/data/migrations/versions/5.10/2022-08-16-14-25-add-subscription-created-events-table.js +11 -0
- package/core/server/data/migrations/versions/5.10/2022-08-19-14-15-fix-comments-deletion-strategy.js +45 -0
- package/core/server/data/schema/schema.js +21 -2
- package/core/server/lib/image/cached-image-size-from-url.js +52 -28
- package/core/server/lib/image/image-utils.js +5 -2
- package/core/server/lib/image/index.js +14 -1
- package/core/server/models/api-key.js +3 -18
- package/core/server/models/base/plugins/actions.js +32 -0
- package/core/server/models/integration.js +3 -0
- package/core/server/models/label.js +3 -18
- package/core/server/models/member-created-event.js +26 -0
- package/core/server/models/member.js +36 -4
- package/core/server/models/post.js +26 -18
- package/core/server/models/subscription-created-event.js +30 -0
- package/core/server/models/tag.js +3 -18
- package/core/server/models/user.js +3 -18
- package/core/server/models/webhook.js +3 -0
- package/core/server/services/comments/emails.js +3 -3
- package/core/server/services/explore/service.js +3 -2
- package/core/server/services/member-attribution/index.js +24 -0
- package/core/server/services/members/api.js +3 -1
- package/core/server/services/members/jobs/clean-expired-comped.js +105 -0
- package/core/server/services/members/jobs/index.js +27 -0
- package/core/server/services/members/service.js +14 -8
- package/core/server/services/settings/settings-service.js +1 -1
- package/core/shared/config/defaults.json +6 -2
- package/core/shared/labs.js +4 -1
- package/package.json +6 -5
- package/yarn.lock +349 -507
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// const debug = require('@tryghost/debug')('comments-counts-assets');
|
|
2
|
+
const Minifier = require('@tryghost/minifier');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs').promises;
|
|
5
|
+
const logging = require('@tryghost/logging');
|
|
6
|
+
const config = require('../../../shared/config');
|
|
7
|
+
|
|
8
|
+
class MemberAttributionAssetsService {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
/** @private */
|
|
11
|
+
this.src = options.src || path.join(config.get('paths').assetSrc, 'member-attribution');
|
|
12
|
+
/** @private */
|
|
13
|
+
this.dest = options.dest || config.getContentPath('public');
|
|
14
|
+
/** @private */
|
|
15
|
+
this.minifier = new Minifier({src: this.src, dest: this.dest});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
generateGlobs() {
|
|
22
|
+
return {
|
|
23
|
+
'member-attribution.min.js': '*.js'
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @private
|
|
29
|
+
*/
|
|
30
|
+
generateReplacements() {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @private
|
|
36
|
+
* @returns {Promise<void>}
|
|
37
|
+
*/
|
|
38
|
+
async minify(globs, options) {
|
|
39
|
+
try {
|
|
40
|
+
await this.minifier.minify(globs, options);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.code === 'EACCES') {
|
|
43
|
+
logging.error('Ghost was not able to write member-attribution asset files due to permissions.');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @private
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
*/
|
|
55
|
+
async clearFiles() {
|
|
56
|
+
const rmFile = async (name) => {
|
|
57
|
+
await fs.unlink(path.join(this.dest, name));
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const promises = [];
|
|
61
|
+
for (const key of Object.keys(this.generateGlobs())) {
|
|
62
|
+
// @deprecated switch this to use fs.rm when we drop support for Node v12
|
|
63
|
+
promises.push(rmFile(key));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// We don't care if removing these files fails as it's valid for them to not exist
|
|
67
|
+
await Promise.allSettled(promises);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Minify, move into the destination directory, and clear existing asset files.
|
|
72
|
+
*
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
75
|
+
async load() {
|
|
76
|
+
const globs = this.generateGlobs();
|
|
77
|
+
const replacements = this.generateReplacements();
|
|
78
|
+
await this.clearFiles();
|
|
79
|
+
await this.minify(globs, {replacements});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = MemberAttributionAssetsService;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Location where we want to store the history in localStorage
|
|
2
|
+
const STORAGE_KEY = 'ghost-history';
|
|
3
|
+
|
|
4
|
+
// How long before an item should expire (24h)
|
|
5
|
+
const TIMEOUT = 24 * 60 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
// Maximum amount of urls in the history
|
|
8
|
+
const LIMIT = 15;
|
|
9
|
+
|
|
10
|
+
// History is saved in JSON format, from old to new
|
|
11
|
+
// Time is saved to be able to exclude old items
|
|
12
|
+
// [
|
|
13
|
+
// {
|
|
14
|
+
// "time": 12341234,
|
|
15
|
+
// "path": "/about/"
|
|
16
|
+
// },
|
|
17
|
+
// {
|
|
18
|
+
// "time": 12341235,
|
|
19
|
+
// "path": "/welcome/"
|
|
20
|
+
// }
|
|
21
|
+
// ]
|
|
22
|
+
|
|
23
|
+
(async function () {
|
|
24
|
+
try {
|
|
25
|
+
const storage = window.localStorage;
|
|
26
|
+
const historyString = storage.getItem(STORAGE_KEY);
|
|
27
|
+
const currentTime = new Date().getTime();
|
|
28
|
+
|
|
29
|
+
// Append current location
|
|
30
|
+
let history = [];
|
|
31
|
+
|
|
32
|
+
if (historyString) {
|
|
33
|
+
try {
|
|
34
|
+
history = JSON.parse(historyString);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
// Ignore invalid JSON, ans clear history
|
|
37
|
+
console.warn('[Member Attribution] Error while parsing history', error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Remove all items that are expired
|
|
42
|
+
const firstNotExpiredIndex = history.findIndex((item) => {
|
|
43
|
+
// Return true to keep all items after and including this item
|
|
44
|
+
// Return false to remove the item
|
|
45
|
+
|
|
46
|
+
if (!item.time || typeof item.time !== 'number') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const difference = currentTime - item.time;
|
|
51
|
+
|
|
52
|
+
if (isNaN(item.time) || difference > TIMEOUT) {
|
|
53
|
+
// Expired or invalid
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Valid item (so all following items are also valid by definition)
|
|
58
|
+
return true;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (firstNotExpiredIndex > 0) {
|
|
62
|
+
// Remove until the first valid item
|
|
63
|
+
history.splice(0, firstNotExpiredIndex);
|
|
64
|
+
} else if (firstNotExpiredIndex === -1) {
|
|
65
|
+
// Not a single valid item found, remove all
|
|
66
|
+
history = [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const currentPath = window.location.pathname;
|
|
70
|
+
|
|
71
|
+
if (history.length === 0 || history[history.length - 1].path !== currentPath) {
|
|
72
|
+
history.push({
|
|
73
|
+
path: currentPath,
|
|
74
|
+
time: currentTime
|
|
75
|
+
});
|
|
76
|
+
} else if (history.length > 0) {
|
|
77
|
+
history[history.length - 1].time = currentTime;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Restrict length
|
|
81
|
+
if (history.length > LIMIT) {
|
|
82
|
+
history = history.slice(-LIMIT);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Save current timestamp
|
|
86
|
+
storage.setItem(STORAGE_KEY, JSON.stringify(history));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('[Member Attribution] Failed with error', error);
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
@@ -75,6 +75,9 @@ module.exports = function setupSiteApp(routerConfig) {
|
|
|
75
75
|
// Comment counts
|
|
76
76
|
siteApp.use(mw.servePublicFile('built', 'public/comment-counts.min.js', 'application/javascript', constants.ONE_YEAR_S));
|
|
77
77
|
|
|
78
|
+
// Member attribution
|
|
79
|
+
siteApp.use(mw.servePublicFile('built', 'public/member-attribution.min.js', 'application/javascript', constants.ONE_YEAR_S));
|
|
80
|
+
|
|
78
81
|
// Serve blog images using the storage adapter
|
|
79
82
|
siteApp.use(STATIC_IMAGE_URL_PREFIX, mw.handleImageSizes, storage.getStorage('images').serve());
|
|
80
83
|
// Serve blog media using the storage adapter
|
|
@@ -2,7 +2,16 @@ const models = require('../../models');
|
|
|
2
2
|
const tpl = require('@tryghost/tpl');
|
|
3
3
|
const errors = require('@tryghost/errors');
|
|
4
4
|
const getPostServiceInstance = require('../../services/posts/posts-service');
|
|
5
|
-
const allowedIncludes = [
|
|
5
|
+
const allowedIncludes = [
|
|
6
|
+
'tags',
|
|
7
|
+
'authors',
|
|
8
|
+
'authors.roles',
|
|
9
|
+
'email',
|
|
10
|
+
'tiers',
|
|
11
|
+
'newsletter',
|
|
12
|
+
'count.signups',
|
|
13
|
+
'count.conversions'
|
|
14
|
+
];
|
|
6
15
|
const unsafeAttrs = ['status', 'authors', 'visibility'];
|
|
7
16
|
|
|
8
17
|
const messages = {
|
|
@@ -23,7 +23,7 @@ function defaultRelations(frame) {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter'];
|
|
26
|
+
frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email', 'tiers', 'newsletter', 'count.signups', 'count.conversions'];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function setDefaultOrder(frame) {
|
|
@@ -10,6 +10,7 @@ const extraAttrs = require('../utils/extra-attrs');
|
|
|
10
10
|
const gating = require('../utils/post-gating');
|
|
11
11
|
const url = require('../utils/url');
|
|
12
12
|
|
|
13
|
+
const labs = require('../../../../../../../shared/labs');
|
|
13
14
|
const utils = require('../../../index');
|
|
14
15
|
|
|
15
16
|
const postsMetaSchema = require('../../../../../../data/schema').tables.posts_meta;
|
|
@@ -109,5 +110,9 @@ module.exports = async (model, frame, options = {}) => {
|
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
if (!labs.isSet('memberAttribution')) {
|
|
114
|
+
delete jsonModel.count;
|
|
115
|
+
}
|
|
116
|
+
|
|
112
117
|
return jsonModel;
|
|
113
118
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const {addTable} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = addTable('members_created_events', {
|
|
4
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
5
|
+
created_at: {type: 'dateTime', nullable: false},
|
|
6
|
+
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
7
|
+
attribution_id: {type: 'string', maxlength: 24, nullable: true},
|
|
8
|
+
attribution_type: {type: 'string', maxlength: 50, nullable: true},
|
|
9
|
+
attribution_url: {type: 'string', maxlength: 2000, nullable: true},
|
|
10
|
+
source: {type: 'string', maxlength: 50, nullable: false}
|
|
11
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const {addTable} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = addTable('members_subscription_created_events', {
|
|
4
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
5
|
+
created_at: {type: 'dateTime', nullable: false},
|
|
6
|
+
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
7
|
+
subscription_id: {type: 'string', maxlength: 24, nullable: false, references: 'members_stripe_customers_subscriptions.id', cascadeDelete: true},
|
|
8
|
+
attribution_id: {type: 'string', maxlength: 24, nullable: true},
|
|
9
|
+
attribution_type: {type: 'string', maxlength: 50, nullable: true},
|
|
10
|
+
attribution_url: {type: 'string', maxlength: 2000, nullable: true}
|
|
11
|
+
});
|
package/core/server/data/migrations/versions/5.10/2022-08-19-14-15-fix-comments-deletion-strategy.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const {addForeign, dropForeign} = require('../../../schema/commands');
|
|
2
|
+
const logging = require('@tryghost/logging');
|
|
3
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = createTransactionalMigration(
|
|
6
|
+
async function up(knex) {
|
|
7
|
+
logging.info('Adding on delete CASCADE for comments parent_id');
|
|
8
|
+
|
|
9
|
+
await dropForeign({
|
|
10
|
+
fromTable: 'comments',
|
|
11
|
+
fromColumn: 'parent_id',
|
|
12
|
+
toTable: 'comments',
|
|
13
|
+
toColumn: 'id',
|
|
14
|
+
transaction: knex
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
await addForeign({
|
|
18
|
+
fromTable: 'comments',
|
|
19
|
+
fromColumn: 'parent_id',
|
|
20
|
+
toTable: 'comments',
|
|
21
|
+
toColumn: 'id',
|
|
22
|
+
cascadeDelete: true,
|
|
23
|
+
transaction: knex
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
async function down(knex) {
|
|
27
|
+
logging.info('Restoring foreign key for comments parent_id');
|
|
28
|
+
|
|
29
|
+
await dropForeign({
|
|
30
|
+
fromTable: 'comments',
|
|
31
|
+
fromColumn: 'parent_id',
|
|
32
|
+
toTable: 'comments',
|
|
33
|
+
toColumn: 'id',
|
|
34
|
+
transaction: knex
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await addForeign({
|
|
38
|
+
fromTable: 'comments',
|
|
39
|
+
fromColumn: 'parent_id',
|
|
40
|
+
toTable: 'comments',
|
|
41
|
+
toColumn: 'id',
|
|
42
|
+
transaction: knex
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
);
|
|
@@ -466,7 +466,8 @@ module.exports = {
|
|
|
466
466
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
467
467
|
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
468
468
|
product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true},
|
|
469
|
-
sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
|
|
469
|
+
sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0},
|
|
470
|
+
expiry_at: {type: 'dateTime', nullable: true}
|
|
470
471
|
},
|
|
471
472
|
posts_products: {
|
|
472
473
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
@@ -474,6 +475,15 @@ module.exports = {
|
|
|
474
475
|
product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true},
|
|
475
476
|
sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
|
|
476
477
|
},
|
|
478
|
+
members_created_events: {
|
|
479
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
480
|
+
created_at: {type: 'dateTime', nullable: false},
|
|
481
|
+
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
482
|
+
attribution_id: {type: 'string', maxlength: 24, nullable: true},
|
|
483
|
+
attribution_type: {type: 'string', maxlength: 50, nullable: true},
|
|
484
|
+
attribution_url: {type: 'string', maxlength: 2000, nullable: true},
|
|
485
|
+
source: {type: 'string', maxlength: 50, nullable: false}
|
|
486
|
+
},
|
|
477
487
|
members_cancel_events: {
|
|
478
488
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
479
489
|
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
@@ -594,6 +604,15 @@ module.exports = {
|
|
|
594
604
|
plan_amount: {type: 'integer', nullable: false},
|
|
595
605
|
plan_currency: {type: 'string', maxLength: 3, nullable: false}
|
|
596
606
|
},
|
|
607
|
+
members_subscription_created_events: {
|
|
608
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
609
|
+
created_at: {type: 'dateTime', nullable: false},
|
|
610
|
+
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
611
|
+
subscription_id: {type: 'string', maxlength: 24, nullable: false, references: 'members_stripe_customers_subscriptions.id', cascadeDelete: true},
|
|
612
|
+
attribution_id: {type: 'string', maxlength: 24, nullable: true},
|
|
613
|
+
attribution_type: {type: 'string', maxlength: 50, nullable: true},
|
|
614
|
+
attribution_url: {type: 'string', maxlength: 2000, nullable: true}
|
|
615
|
+
},
|
|
597
616
|
offer_redemptions: {
|
|
598
617
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
599
618
|
offer_id: {type: 'string', maxlength: 24, nullable: false, references: 'offers.id', cascadeDelete: true},
|
|
@@ -754,7 +773,7 @@ module.exports = {
|
|
|
754
773
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
755
774
|
post_id: {type: 'string', maxlength: 24, nullable: false, unique: false, references: 'posts.id', cascadeDelete: true},
|
|
756
775
|
member_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'members.id', setNullDelete: true},
|
|
757
|
-
parent_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'comments.id'},
|
|
776
|
+
parent_id: {type: 'string', maxlength: 24, nullable: true, unique: false, references: 'comments.id', cascadeDelete: true},
|
|
758
777
|
status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'published', validations: {isIn: [['published', 'hidden', 'deleted']]}},
|
|
759
778
|
html: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
|
|
760
779
|
edited_at: {type: 'dateTime', nullable: true},
|
|
@@ -1,52 +1,76 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('utils:image-size-cache');
|
|
2
2
|
const errors = require('@tryghost/errors');
|
|
3
3
|
const logging = require('@tryghost/logging');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @example
|
|
7
|
+
* {
|
|
8
|
+
* height: 50,
|
|
9
|
+
* url: 'https://mysite.com/images/cat.jpg',
|
|
10
|
+
* width: 50
|
|
11
|
+
* }
|
|
12
|
+
* @typedef ImageSizeCache
|
|
13
|
+
* @type {Object}
|
|
14
|
+
* @property {string} url image url
|
|
15
|
+
* @property {number} height image height
|
|
16
|
+
* @property {number} width image width
|
|
17
|
+
*/
|
|
18
|
+
|
|
4
19
|
class CachedImageSizeFromUrl {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
20
|
+
/**
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {(url: string) => Promise<ImageSizeCache>} options.getImageSizeFromUrl - method that resolves images based on URL
|
|
24
|
+
* @param {Object} options.cache - cache store instance
|
|
25
|
+
*/
|
|
26
|
+
constructor({getImageSizeFromUrl, cache}) {
|
|
27
|
+
this.getImageSizeFromUrl = getImageSizeFromUrl;
|
|
28
|
+
this.cache = cache;
|
|
8
29
|
}
|
|
9
30
|
|
|
10
31
|
/**
|
|
11
32
|
* Get cached image size from URL
|
|
12
33
|
* Always returns {object} imageSizeCache
|
|
13
34
|
* @param {string} url
|
|
14
|
-
* @returns {Promise<
|
|
35
|
+
* @returns {Promise<ImageSizeCache>}
|
|
15
36
|
* @description Takes a url and returns image width and height from cache if available.
|
|
16
37
|
* If not in cache, `getImageSizeFromUrl` is called and returns the dimensions in a Promise.
|
|
17
38
|
*/
|
|
18
|
-
getCachedImageSizeFromUrl(url) {
|
|
39
|
+
async getCachedImageSizeFromUrl(url) {
|
|
19
40
|
if (!url || url === undefined || url === null) {
|
|
20
41
|
return;
|
|
21
42
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
|
|
44
|
+
const cachedImageSize = await this.cache.get(url);
|
|
45
|
+
|
|
46
|
+
if (cachedImageSize) {
|
|
47
|
+
debug('Read image from cache:', url);
|
|
48
|
+
|
|
49
|
+
return cachedImageSize;
|
|
50
|
+
} else {
|
|
51
|
+
try {
|
|
52
|
+
const res = await this.getImageSizeFromUrl(url);
|
|
53
|
+
await this.cache.set(url, res);
|
|
54
|
+
|
|
28
55
|
debug('Cached image:', url);
|
|
29
|
-
|
|
30
|
-
return this.cache.get(url);
|
|
31
|
-
}).catch(errors.NotFoundError, () => {
|
|
32
|
-
debug('Cached image (not found):', url);
|
|
33
|
-
// in case of error we just attach the url
|
|
34
|
-
this.cache.set(url, url);
|
|
35
|
-
|
|
56
|
+
|
|
36
57
|
return this.cache.get(url);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err instanceof errors.NotFoundError) {
|
|
60
|
+
debug('Cached image (not found):', url);
|
|
61
|
+
} else {
|
|
62
|
+
debug('Cached image (error):', url);
|
|
63
|
+
logging.error(err);
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
// in case of error we just attach the url
|
|
42
|
-
this.cache.set(url,
|
|
43
|
-
|
|
67
|
+
await this.cache.set(url, {
|
|
68
|
+
url
|
|
69
|
+
});
|
|
70
|
+
|
|
44
71
|
return this.cache.get(url);
|
|
45
|
-
}
|
|
72
|
+
}
|
|
46
73
|
}
|
|
47
|
-
debug('Read image from cache:', url);
|
|
48
|
-
// returns image size from cache
|
|
49
|
-
return this.cache.get(url);
|
|
50
74
|
}
|
|
51
75
|
}
|
|
52
76
|
|
|
@@ -4,10 +4,13 @@ const Gravatar = require('./gravatar');
|
|
|
4
4
|
const ImageSize = require('./image-size');
|
|
5
5
|
|
|
6
6
|
class ImageUtils {
|
|
7
|
-
constructor({config, urlUtils, settingsCache, storageUtils, storage, validator, request}) {
|
|
7
|
+
constructor({config, urlUtils, settingsCache, storageUtils, storage, validator, request, cacheStore}) {
|
|
8
8
|
this.blogIcon = new BlogIcon({config, urlUtils, settingsCache, storageUtils});
|
|
9
9
|
this.imageSize = new ImageSize({config, storage, storageUtils, validator, urlUtils, request});
|
|
10
|
-
this.cachedImageSizeFromUrl = new CachedImageSizeFromUrl({
|
|
10
|
+
this.cachedImageSizeFromUrl = new CachedImageSizeFromUrl({
|
|
11
|
+
getImageSizeFromUrl: this.imageSize.getImageSizeFromUrl.bind(this.imageSize),
|
|
12
|
+
cache: cacheStore
|
|
13
|
+
});
|
|
11
14
|
this.gravatar = new Gravatar({config, request});
|
|
12
15
|
}
|
|
13
16
|
}
|
|
@@ -7,4 +7,17 @@ const config = require('../../../shared/config');
|
|
|
7
7
|
const settingsCache = require('../../../shared/settings-cache');
|
|
8
8
|
const ImageUtils = require('./image-utils');
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const adapterManager = require('../../services/adapter-manager');
|
|
11
|
+
|
|
12
|
+
const cacheStore = adapterManager.getAdapter('cache:imageSizes');
|
|
13
|
+
|
|
14
|
+
module.exports = new ImageUtils({
|
|
15
|
+
config,
|
|
16
|
+
urlUtils,
|
|
17
|
+
settingsCache,
|
|
18
|
+
storageUtils,
|
|
19
|
+
storage,
|
|
20
|
+
validator,
|
|
21
|
+
request,
|
|
22
|
+
cacheStore
|
|
23
|
+
});
|
|
@@ -6,6 +6,9 @@ const {Role} = require('./role');
|
|
|
6
6
|
const ApiKey = ghostBookshelf.Model.extend({
|
|
7
7
|
tableName: 'api_keys',
|
|
8
8
|
|
|
9
|
+
actionsCollectCRUD: true,
|
|
10
|
+
actionsResourceType: 'api_key',
|
|
11
|
+
|
|
9
12
|
defaults() {
|
|
10
13
|
const secret = security.secret.create(this.get('type'));
|
|
11
14
|
|
|
@@ -53,24 +56,6 @@ const ApiKey = ghostBookshelf.Model.extend({
|
|
|
53
56
|
if (this.previous('secret') !== this.get('secret')) {
|
|
54
57
|
this.addAction(model, 'refreshed', options);
|
|
55
58
|
}
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
getAction(event, options) {
|
|
59
|
-
const actor = this.getActor(options);
|
|
60
|
-
|
|
61
|
-
// @NOTE: we ignore internal updates (`options.context.internal`) for now
|
|
62
|
-
if (!actor) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// @TODO: implement context
|
|
67
|
-
return {
|
|
68
|
-
event: event,
|
|
69
|
-
resource_id: this.id || this.previous('id'),
|
|
70
|
-
resource_type: 'api_key',
|
|
71
|
-
actor_id: actor.id,
|
|
72
|
-
actor_type: actor.type
|
|
73
|
-
};
|
|
74
59
|
}
|
|
75
60
|
}, {
|
|
76
61
|
refreshSecret(data, options) {
|
|
@@ -7,6 +7,38 @@ const logging = require('@tryghost/logging');
|
|
|
7
7
|
*/
|
|
8
8
|
module.exports = function (Bookshelf) {
|
|
9
9
|
Bookshelf.Model = Bookshelf.Model.extend({
|
|
10
|
+
/**
|
|
11
|
+
* Constructs data to be stored in the database with info
|
|
12
|
+
* on particular actions
|
|
13
|
+
*/
|
|
14
|
+
getAction(event, options) {
|
|
15
|
+
const actor = this.getActor(options);
|
|
16
|
+
|
|
17
|
+
// @NOTE: we ignore internal updates (`options.context.internal`) for now
|
|
18
|
+
if (!actor) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!this.actionsCollectCRUD) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let resourceType = this.actionsResourceType;
|
|
27
|
+
|
|
28
|
+
if (typeof resourceType === 'function') {
|
|
29
|
+
resourceType = resourceType.bind(this)();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// @TODO: implement context
|
|
33
|
+
return {
|
|
34
|
+
event: event,
|
|
35
|
+
resource_id: this.id || this.previous('id'),
|
|
36
|
+
resource_type: resourceType,
|
|
37
|
+
actor_id: actor.id,
|
|
38
|
+
actor_type: actor.type
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
|
|
10
42
|
/**
|
|
11
43
|
* @NOTE:
|
|
12
44
|
*
|
|
@@ -6,6 +6,9 @@ const {NoPermissionError} = require('@tryghost/errors');
|
|
|
6
6
|
const Integration = ghostBookshelf.Model.extend({
|
|
7
7
|
tableName: 'integrations',
|
|
8
8
|
|
|
9
|
+
actionsCollectCRUD: true,
|
|
10
|
+
actionsResourceType: 'integration',
|
|
11
|
+
|
|
9
12
|
relationships: ['api_keys', 'webhooks'],
|
|
10
13
|
|
|
11
14
|
relationshipBelongsTo: {
|