ghost 5.112.0 → 5.113.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-cache-redis-5.112.0.tgz → tryghost-adapter-cache-redis-5.113.0.tgz} +0 -0
- package/components/{tryghost-adapter-manager-5.112.0.tgz → tryghost-adapter-manager-5.113.0.tgz} +0 -0
- package/components/tryghost-announcement-bar-settings-5.113.0.tgz +0 -0
- package/components/{tryghost-api-framework-5.112.0.tgz → tryghost-api-framework-5.113.0.tgz} +0 -0
- package/components/{tryghost-api-version-compatibility-service-5.112.0.tgz → tryghost-api-version-compatibility-service-5.113.0.tgz} +0 -0
- package/components/{tryghost-audience-feedback-5.112.0.tgz → tryghost-audience-feedback-5.113.0.tgz} +0 -0
- package/components/tryghost-bookshelf-repository-5.113.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.113.0.tgz +0 -0
- package/components/tryghost-captcha-service-5.113.0.tgz +0 -0
- package/components/tryghost-constants-5.113.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.113.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.113.0.tgz +0 -0
- package/components/{tryghost-data-generator-5.112.0.tgz → tryghost-data-generator-5.113.0.tgz} +0 -0
- package/components/tryghost-domain-events-5.113.0.tgz +0 -0
- package/components/{tryghost-donations-5.112.0.tgz → tryghost-donations-5.113.0.tgz} +0 -0
- package/components/tryghost-email-addresses-5.113.0.tgz +0 -0
- package/components/{tryghost-email-analytics-provider-mailgun-5.112.0.tgz → tryghost-email-analytics-provider-mailgun-5.113.0.tgz} +0 -0
- package/components/tryghost-email-analytics-service-5.113.0.tgz +0 -0
- package/components/{tryghost-email-content-generator-5.112.0.tgz → tryghost-email-content-generator-5.113.0.tgz} +0 -0
- package/components/tryghost-email-events-5.113.0.tgz +0 -0
- package/components/{tryghost-email-service-5.112.0.tgz → tryghost-email-service-5.113.0.tgz} +0 -0
- package/components/{tryghost-email-suppression-list-5.112.0.tgz → tryghost-email-suppression-list-5.113.0.tgz} +0 -0
- package/components/{tryghost-express-dynamic-redirects-5.112.0.tgz → tryghost-express-dynamic-redirects-5.113.0.tgz} +0 -0
- package/components/tryghost-extract-api-key-5.113.0.tgz +0 -0
- package/components/{tryghost-ghost-5.112.0.tgz → tryghost-ghost-5.113.0.tgz} +0 -0
- package/components/{tryghost-html-to-plaintext-5.112.0.tgz → tryghost-html-to-plaintext-5.113.0.tgz} +0 -0
- package/components/tryghost-i18n-5.113.0.tgz +0 -0
- package/components/{tryghost-identity-token-service-5.112.0.tgz → tryghost-identity-token-service-5.113.0.tgz} +0 -0
- package/components/{tryghost-importer-handler-content-files-5.112.0.tgz → tryghost-importer-handler-content-files-5.113.0.tgz} +0 -0
- package/components/{tryghost-importer-revue-5.112.0.tgz → tryghost-importer-revue-5.113.0.tgz} +0 -0
- package/components/{tryghost-in-memory-repository-5.112.0.tgz → tryghost-in-memory-repository-5.113.0.tgz} +0 -0
- package/components/tryghost-job-manager-5.113.0.tgz +0 -0
- package/components/{tryghost-link-redirects-5.112.0.tgz → tryghost-link-redirects-5.113.0.tgz} +0 -0
- package/components/{tryghost-link-replacer-5.112.0.tgz → tryghost-link-replacer-5.113.0.tgz} +0 -0
- package/components/{tryghost-magic-link-5.112.0.tgz → tryghost-magic-link-5.113.0.tgz} +0 -0
- package/components/{tryghost-mail-events-5.112.0.tgz → tryghost-mail-events-5.113.0.tgz} +0 -0
- package/components/tryghost-mailgun-client-5.113.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.113.0.tgz +0 -0
- package/components/tryghost-member-events-5.113.0.tgz +0 -0
- package/components/{tryghost-members-api-5.112.0.tgz → tryghost-members-api-5.113.0.tgz} +0 -0
- package/components/tryghost-members-csv-5.113.0.tgz +0 -0
- package/components/{tryghost-members-importer-5.112.0.tgz → tryghost-members-importer-5.113.0.tgz} +0 -0
- package/components/{tryghost-members-offers-5.112.0.tgz → tryghost-members-offers-5.113.0.tgz} +0 -0
- package/components/tryghost-members-payments-5.113.0.tgz +0 -0
- package/components/{tryghost-members-ssr-5.112.0.tgz → tryghost-members-ssr-5.113.0.tgz} +0 -0
- package/components/{tryghost-members-stripe-service-5.112.0.tgz → tryghost-members-stripe-service-5.113.0.tgz} +0 -0
- package/components/tryghost-milestones-5.113.0.tgz +0 -0
- package/components/{tryghost-minifier-5.112.0.tgz → tryghost-minifier-5.113.0.tgz} +0 -0
- package/components/{tryghost-mw-api-version-mismatch-5.112.0.tgz → tryghost-mw-api-version-mismatch-5.113.0.tgz} +0 -0
- package/components/tryghost-mw-cache-control-5.113.0.tgz +0 -0
- package/components/{tryghost-mw-error-handler-5.112.0.tgz → tryghost-mw-error-handler-5.113.0.tgz} +0 -0
- package/components/{tryghost-mw-session-from-token-5.112.0.tgz → tryghost-mw-session-from-token-5.113.0.tgz} +0 -0
- package/components/{tryghost-mw-update-user-last-seen-5.112.0.tgz → tryghost-mw-update-user-last-seen-5.113.0.tgz} +0 -0
- package/components/tryghost-mw-version-match-5.113.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.113.0.tgz +0 -0
- package/components/{tryghost-package-json-5.112.0.tgz → tryghost-package-json-5.113.0.tgz} +0 -0
- package/components/{tryghost-post-events-5.112.0.tgz → tryghost-post-events-5.113.0.tgz} +0 -0
- package/components/tryghost-post-revisions-5.113.0.tgz +0 -0
- package/components/{tryghost-posts-service-5.112.0.tgz → tryghost-posts-service-5.113.0.tgz} +0 -0
- package/components/{tryghost-prometheus-metrics-5.112.0.tgz → tryghost-prometheus-metrics-5.113.0.tgz} +0 -0
- package/components/tryghost-recommendations-5.113.0.tgz +0 -0
- package/components/tryghost-referrers-5.113.0.tgz +0 -0
- package/components/{tryghost-security-5.112.0.tgz → tryghost-security-5.113.0.tgz} +0 -0
- package/components/tryghost-session-service-5.113.0.tgz +0 -0
- package/components/{tryghost-settings-path-manager-5.112.0.tgz → tryghost-settings-path-manager-5.113.0.tgz} +0 -0
- package/components/{tryghost-slack-notifications-5.112.0.tgz → tryghost-slack-notifications-5.113.0.tgz} +0 -0
- package/components/tryghost-tiers-5.113.0.tgz +0 -0
- package/components/{tryghost-version-notifications-data-service-5.112.0.tgz → tryghost-version-notifications-data-service-5.113.0.tgz} +0 -0
- package/components/{tryghost-webmentions-5.112.0.tgz → tryghost-webmentions-5.113.0.tgz} +0 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +12953 -12243
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-ad8698fe.mjs → CodeEditorView-ed5e87be.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-463cec50.mjs → index-0ee4d13c.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-2713e469.mjs → index-9c7da716.mjs} +19975 -19965
- package/core/built/admin/assets/admin-x-settings/{modals-033e8fc4.mjs → modals-7708d510.mjs} +2226 -2215
- package/core/built/admin/assets/{chunk.524.db49da6fd8ae155205a4.js → chunk.524.4f0aeb6b611079e528f5.js} +7 -7
- package/core/built/admin/assets/{chunk.582.0bf715eb6807f7641706.js → chunk.582.485df00698ed27a0668b.js} +9 -9
- package/core/built/admin/assets/{ghost-62bd4d4c837d453e1038808dc1cd1e4c.js → ghost-ebf07ae7768b6e9fb9a4b173b6917782.js} +66 -66
- package/core/built/admin/assets/posts/posts.js +1 -1
- package/core/built/admin/assets/{vendor-fca15534b8426c0567400113c63a3e21.js → vendor-68a4aa424a179a90f5bbc2b750def576.js} +28 -26
- package/core/built/admin/index.html +4 -4
- package/core/frontend/services/routing/registry.js +6 -6
- package/core/frontend/src/admin-auth/message-handler.js +1 -1
- package/core/server/adapters/cache/AdapterCacheMemoryTTL.js +54 -0
- package/core/server/adapters/cache/memory-ttl.js +1 -1
- package/core/server/data/migrations/versions/5.113/2025-03-07-12-24-00-add-super-editor.js +31 -0
- package/core/server/data/migrations/versions/5.113/2025-03-07-12-25-00-add-member-perms-to-super-editor.js +291 -0
- package/core/server/data/schema/fixtures/fixtures.json +27 -0
- package/core/server/models/invite.js +2 -2
- package/core/server/models/role.js +2 -2
- package/core/server/models/user.js +39 -28
- package/core/server/services/email-analytics/jobs/update-member-email-analytics/index.js +13 -0
- package/core/server/services/email-analytics/lib/queries.js +3 -3
- package/core/server/services/media-inliner/ExternalMediaInliner.js +346 -0
- package/core/server/services/media-inliner/service.js +1 -1
- package/core/server/services/permissions/can-this.js +3 -2
- package/core/server/services/url/Resources.js +19 -29
- package/core/server/services/url/UrlService.js +2 -12
- package/core/server/services/url/Urls.js +17 -33
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/labs.js +2 -1
- package/core/shared/settings-cache/CacheManager.js +4 -4
- package/package.json +134 -134
- package/yarn.lock +10 -10
- package/components/tryghost-adapter-cache-memory-ttl-5.112.0.tgz +0 -0
- package/components/tryghost-announcement-bar-settings-5.112.0.tgz +0 -0
- package/components/tryghost-bookshelf-repository-5.112.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-5.112.0.tgz +0 -0
- package/components/tryghost-captcha-service-5.112.0.tgz +0 -0
- package/components/tryghost-constants-5.112.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.112.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.112.0.tgz +0 -0
- package/components/tryghost-domain-events-5.112.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.112.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-5.112.0.tgz +0 -0
- package/components/tryghost-email-events-5.112.0.tgz +0 -0
- package/components/tryghost-external-media-inliner-5.112.0.tgz +0 -0
- package/components/tryghost-extract-api-key-5.112.0.tgz +0 -0
- package/components/tryghost-i18n-5.112.0.tgz +0 -0
- package/components/tryghost-job-manager-5.112.0.tgz +0 -0
- package/components/tryghost-mailgun-client-5.112.0.tgz +0 -0
- package/components/tryghost-member-attribution-5.112.0.tgz +0 -0
- package/components/tryghost-member-events-5.112.0.tgz +0 -0
- package/components/tryghost-members-csv-5.112.0.tgz +0 -0
- package/components/tryghost-members-payments-5.112.0.tgz +0 -0
- package/components/tryghost-milestones-5.112.0.tgz +0 -0
- package/components/tryghost-mw-cache-control-5.112.0.tgz +0 -0
- package/components/tryghost-mw-version-match-5.112.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.112.0.tgz +0 -0
- package/components/tryghost-post-revisions-5.112.0.tgz +0 -0
- package/components/tryghost-recommendations-5.112.0.tgz +0 -0
- package/components/tryghost-referrers-5.112.0.tgz +0 -0
- package/components/tryghost-session-service-5.112.0.tgz +0 -0
- package/components/tryghost-tiers-5.112.0.tgz +0 -0
|
@@ -99,7 +99,7 @@ module.exports = {
|
|
|
99
99
|
/**
|
|
100
100
|
* Sets the timestamp of the last seen event for the specified email analytics events.
|
|
101
101
|
* @param {EmailAnalyticsJobName} jobName - The name of the job to update.
|
|
102
|
-
* @param {'
|
|
102
|
+
* @param {'finished'|'started'} field - The field to update.
|
|
103
103
|
* @param {Date} date - The timestamp of the last seen event.
|
|
104
104
|
* @returns {Promise<void>}
|
|
105
105
|
* @description
|
|
@@ -110,8 +110,8 @@ module.exports = {
|
|
|
110
110
|
// Convert string dates to Date objects for SQLite compatibility
|
|
111
111
|
try {
|
|
112
112
|
debug(`Setting ${field} timestamp for job ${jobName} to ${date}`);
|
|
113
|
-
const updateField = field === '
|
|
114
|
-
const status = field === '
|
|
113
|
+
const updateField = field === 'finished' ? 'finished_at' : 'started_at';
|
|
114
|
+
const status = field === 'finished' ? 'finished' : 'started';
|
|
115
115
|
const result = await db.knex('jobs').update({[updateField]: date, updated_at: new Date(), status: status}).where('name', jobName);
|
|
116
116
|
if (result === 0) {
|
|
117
117
|
await db.knex('jobs').insert({
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
const mime = require('mime-types');
|
|
2
|
+
const FileType = require('file-type');
|
|
3
|
+
const request = require('@tryghost/request');
|
|
4
|
+
const errors = require('@tryghost/errors');
|
|
5
|
+
const logging = require('@tryghost/logging');
|
|
6
|
+
const string = require('@tryghost/string');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
class ExternalMediaInliner {
|
|
10
|
+
/** @type {object} */
|
|
11
|
+
#PostModel;
|
|
12
|
+
|
|
13
|
+
/** @type {object} */
|
|
14
|
+
#PostMetaModel;
|
|
15
|
+
|
|
16
|
+
/** @type {object} */
|
|
17
|
+
#TagModel;
|
|
18
|
+
|
|
19
|
+
/** @type {object} */
|
|
20
|
+
#UserModel;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} deps
|
|
25
|
+
* @param {Object} deps.PostModel - Post model
|
|
26
|
+
* @param {Object} deps.PostMetaModel - PostMeta model
|
|
27
|
+
* @param {Object} deps.TagModel - Tag model
|
|
28
|
+
* @param {Object} deps.UserModel - User model
|
|
29
|
+
* @param {(extension) => import('ghost-storage-base')} deps.getMediaStorage - getMediaStorage
|
|
30
|
+
*/
|
|
31
|
+
constructor(deps) {
|
|
32
|
+
this.#PostModel = deps.PostModel;
|
|
33
|
+
this.#PostMetaModel = deps.PostMetaModel;
|
|
34
|
+
this.#TagModel = deps.TagModel;
|
|
35
|
+
this.#UserModel = deps.UserModel;
|
|
36
|
+
this.getMediaStorage = deps.getMediaStorage;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
*
|
|
41
|
+
* @param {string} requestURL - url of remote media
|
|
42
|
+
* @returns {Promise<Object>}
|
|
43
|
+
*/
|
|
44
|
+
async getRemoteMedia(requestURL) {
|
|
45
|
+
// @NOTE: this is the most expensive operation in the whole inlining process
|
|
46
|
+
// we should consider caching the results to improve performance
|
|
47
|
+
|
|
48
|
+
// Enforce http - http > https redirects are commonplace
|
|
49
|
+
requestURL = requestURL.replace(/^\/\//g, 'http://');
|
|
50
|
+
|
|
51
|
+
// Encode to handle special characters in URLs
|
|
52
|
+
requestURL = encodeURI(requestURL);
|
|
53
|
+
try {
|
|
54
|
+
const response = await request(requestURL, {
|
|
55
|
+
followRedirect: true,
|
|
56
|
+
responseType: 'buffer'
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return response;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// NOTE: add special case for 404s
|
|
62
|
+
logging.error(`Error downloading remote media: ${requestURL}`);
|
|
63
|
+
logging.error(new errors.DataImportError({
|
|
64
|
+
err: error
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} response - response from request
|
|
74
|
+
* @returns {Object}
|
|
75
|
+
*/
|
|
76
|
+
async extractFileDataFromResponse(requestURL, response) {
|
|
77
|
+
let extension;
|
|
78
|
+
|
|
79
|
+
// Attempt to get the file extension from the file itself
|
|
80
|
+
// If that fails, or if `.ext` is undefined, get the extension from the file path in the catch
|
|
81
|
+
try {
|
|
82
|
+
const fileInfo = await FileType.fromBuffer(response.body);
|
|
83
|
+
extension = fileInfo.ext;
|
|
84
|
+
} catch {
|
|
85
|
+
const headers = response.headers;
|
|
86
|
+
const contentType = headers['content-type'];
|
|
87
|
+
const extensionFromPath = path.parse(requestURL).ext.split(/[^a-z]/i).filter(Boolean)[0];
|
|
88
|
+
extension = mime.extension(contentType) || extensionFromPath;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const removeExtRegExp = new RegExp(`.${extension}`, '');
|
|
92
|
+
const fileNameNoExt = path.parse(requestURL).base.replace(removeExtRegExp, '');
|
|
93
|
+
|
|
94
|
+
// CASE: Query strings _can_ form part of the unique image URL, so rather that strip them include the in the file name
|
|
95
|
+
// Then trim to last 248 chars (this will be more unique than the first 248), and trim leading & trailing dashes.
|
|
96
|
+
// 248 is on the lower end of limits from various OSes and file systems
|
|
97
|
+
const fileName = string.slugify(path.parse(fileNameNoExt).base, {
|
|
98
|
+
requiredChangesOnly: true
|
|
99
|
+
}).slice(-248).replace(/^-|-$/, '');
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
fileBuffer: response.body,
|
|
103
|
+
filename: `${fileName}.${extension}`,
|
|
104
|
+
extension: `.${extension}`
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} media - media to store locally
|
|
111
|
+
* @returns {Promise<string>} - path to stored media
|
|
112
|
+
*/
|
|
113
|
+
async storeMediaLocally(media) {
|
|
114
|
+
const storage = this.getMediaStorage(media.extension);
|
|
115
|
+
|
|
116
|
+
if (!storage) {
|
|
117
|
+
logging.warn(`No storage adapter found for file extension: ${media.extension}`);
|
|
118
|
+
return null;
|
|
119
|
+
} else {
|
|
120
|
+
// @NOTE: this is extremely convoluted and should live on a
|
|
121
|
+
// storage adapter level
|
|
122
|
+
const targetDir = storage.getTargetDir(storage.storagePath);
|
|
123
|
+
const uniqueFileName = await storage.getUniqueFileName({
|
|
124
|
+
name: media.filename
|
|
125
|
+
}, targetDir);
|
|
126
|
+
const targetPath = path.relative(storage.storagePath, uniqueFileName);
|
|
127
|
+
const filePath = await storage.saveRaw(media.fileBuffer, targetPath);
|
|
128
|
+
return filePath;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static findMatches(content, domain) {
|
|
133
|
+
// NOTE: the src could end with a quote, bracket, apostrophe, double-backslash, or encoded quote.
|
|
134
|
+
// Backlashes are added to content as an escape character
|
|
135
|
+
const srcTerminationSymbols = `("|\\)|'|(?=(?:,https?))| |<|\\\\|"|$)`;
|
|
136
|
+
const regex = new RegExp(`(${domain}.*?)(${srcTerminationSymbols})`, 'igm');
|
|
137
|
+
const matches = content.matchAll(regex);
|
|
138
|
+
|
|
139
|
+
// Simplify the matches so we only get the result needed
|
|
140
|
+
let matchesArray = Array.from(matches, m => m[1]);
|
|
141
|
+
|
|
142
|
+
// Trim trailing commas from each match
|
|
143
|
+
matchesArray = matchesArray.map((item) => {
|
|
144
|
+
return item.replace(/,$/, '');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return matchesArray;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Find & inline external media from a JSON sting.
|
|
152
|
+
* This works with both Lexical & Mobiledoc, so no separate methods are needed here.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} content - stringified JSON of post Lexical or Mobiledoc content
|
|
155
|
+
* @param {String[]} domains - domains to inline media from
|
|
156
|
+
* @returns {Promise<string>} - updated stringified JSON of post content
|
|
157
|
+
*/
|
|
158
|
+
async inlineContent(content, domains) {
|
|
159
|
+
for (const domain of domains) {
|
|
160
|
+
const matches = this.constructor.findMatches(content, domain);
|
|
161
|
+
|
|
162
|
+
for (const src of matches) {
|
|
163
|
+
const response = await this.getRemoteMedia(src);
|
|
164
|
+
|
|
165
|
+
let media;
|
|
166
|
+
if (response) {
|
|
167
|
+
media = await this.extractFileDataFromResponse(src, response);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (media) {
|
|
171
|
+
const filePath = await this.storeMediaLocally(media);
|
|
172
|
+
|
|
173
|
+
if (filePath) {
|
|
174
|
+
const inlinedSrc = `__GHOST_URL__${filePath}`;
|
|
175
|
+
|
|
176
|
+
// NOTE: does not account for duplicate images in content
|
|
177
|
+
// in those cases would be processed twice
|
|
178
|
+
content = content.replace(src, inlinedSrc);
|
|
179
|
+
logging.info(`Inlined media: ${src} -> ${inlinedSrc}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return content;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
*
|
|
190
|
+
* @param {Object} resourceModel - one of PostModel, TagModel, UserModel instances
|
|
191
|
+
* @param {String[]} fields - fields to inline
|
|
192
|
+
* @param {String[]} domains - domains to inline media from
|
|
193
|
+
* @returns Promise<Object> - updated fields map with local media paths
|
|
194
|
+
*/
|
|
195
|
+
async inlineFields(resourceModel, fields, domains) {
|
|
196
|
+
const updatedFields = {};
|
|
197
|
+
|
|
198
|
+
for (const field of fields) {
|
|
199
|
+
for (const domain of domains) {
|
|
200
|
+
const src = resourceModel.get(field);
|
|
201
|
+
|
|
202
|
+
if (src && src.startsWith(domain)) {
|
|
203
|
+
const response = await this.getRemoteMedia(src);
|
|
204
|
+
|
|
205
|
+
let media;
|
|
206
|
+
if (response) {
|
|
207
|
+
media = await this.extractFileDataFromResponse(src, response);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (media) {
|
|
211
|
+
const filePath = await this.storeMediaLocally(media);
|
|
212
|
+
|
|
213
|
+
if (filePath) {
|
|
214
|
+
const inlinedSrc = `__GHOST_URL__${filePath}`;
|
|
215
|
+
|
|
216
|
+
updatedFields[field] = inlinedSrc;
|
|
217
|
+
logging.info(`Added media to inline: ${src} -> ${inlinedSrc}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return updatedFields;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
*
|
|
229
|
+
* @param {Object[]} resources - array of model instances
|
|
230
|
+
* @param {Object} model - resource model
|
|
231
|
+
* @param {string[]} fields - fields to inline
|
|
232
|
+
* @param {string[]} domains - domains to inline media from
|
|
233
|
+
*/
|
|
234
|
+
async inlineSimpleFields(resources, model, fields, domains) {
|
|
235
|
+
logging.info(`Starting inlining external media for ${resources?.length} resources and with ${fields.join(', ')} fields`);
|
|
236
|
+
|
|
237
|
+
for (const resource of resources) {
|
|
238
|
+
try {
|
|
239
|
+
const updatedFields = await this.inlineFields(resource, fields, domains);
|
|
240
|
+
|
|
241
|
+
if (Object.keys(updatedFields).length > 0) {
|
|
242
|
+
await model.edit(updatedFields, {
|
|
243
|
+
id: resource.id,
|
|
244
|
+
context: {
|
|
245
|
+
internal: true
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
} catch (err) {
|
|
250
|
+
logging.error(`Error inlining media for: ${resource.id}`);
|
|
251
|
+
logging.error(new errors.DataImportError({
|
|
252
|
+
err
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
*
|
|
260
|
+
* @param {string[]} domains domains to inline media from
|
|
261
|
+
*/
|
|
262
|
+
async inline(domains) {
|
|
263
|
+
const posts = await this.#PostModel.findAll({context: {internal: true}});
|
|
264
|
+
const postsInilingFields = [
|
|
265
|
+
'feature_image'
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
logging.info(`Starting inlining external media for posts: ${posts?.length}`);
|
|
269
|
+
|
|
270
|
+
for (const post of posts) {
|
|
271
|
+
try {
|
|
272
|
+
const mobiledocContent = post.get('mobiledoc');
|
|
273
|
+
const lexicalContent = post.get('lexical');
|
|
274
|
+
|
|
275
|
+
const updatedFields = await this.inlineFields(post, postsInilingFields, domains);
|
|
276
|
+
|
|
277
|
+
if (mobiledocContent) {
|
|
278
|
+
const inlinedContent = await this.inlineContent(mobiledocContent, domains);
|
|
279
|
+
|
|
280
|
+
// If content has changed, update the post
|
|
281
|
+
if (inlinedContent !== mobiledocContent) {
|
|
282
|
+
updatedFields.mobiledoc = inlinedContent;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (lexicalContent) {
|
|
287
|
+
const inlinedContent = await this.inlineContent(lexicalContent, domains);
|
|
288
|
+
|
|
289
|
+
// If content has changed, update the post
|
|
290
|
+
if (inlinedContent !== lexicalContent) {
|
|
291
|
+
updatedFields.lexical = inlinedContent;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (Object.keys(updatedFields).length > 0) {
|
|
296
|
+
await this.#PostModel.edit(updatedFields, {
|
|
297
|
+
id: post.id,
|
|
298
|
+
context: {
|
|
299
|
+
internal: true
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
logging.error(`Error inlining media for post: ${post.id}`);
|
|
305
|
+
logging.error(new errors.DataImportError({
|
|
306
|
+
err
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const {data: postsMetas} = await this.#PostMetaModel.findPage({
|
|
312
|
+
limit: 'all'
|
|
313
|
+
});
|
|
314
|
+
const postsMetaInilingFields = [
|
|
315
|
+
'og_image',
|
|
316
|
+
'twitter_image'
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
await this.inlineSimpleFields(postsMetas, this.#PostMetaModel, postsMetaInilingFields, domains);
|
|
320
|
+
|
|
321
|
+
const {data: tags} = await this.#TagModel.findPage({
|
|
322
|
+
limit: 'all'
|
|
323
|
+
});
|
|
324
|
+
const tagInliningFields = [
|
|
325
|
+
'feature_image',
|
|
326
|
+
'og_image',
|
|
327
|
+
'twitter_image'
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
await this.inlineSimpleFields(tags, this.#TagModel, tagInliningFields, domains);
|
|
331
|
+
|
|
332
|
+
const {data: users} = await this.#UserModel.findPage({
|
|
333
|
+
limit: 'all'
|
|
334
|
+
});
|
|
335
|
+
const userInliningFields = [
|
|
336
|
+
'profile_image',
|
|
337
|
+
'cover_image'
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
await this.inlineSimpleFields(users, this.#UserModel, userInliningFields, domains);
|
|
341
|
+
|
|
342
|
+
logging.info('Finished inlining external media for posts, tags, and users');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = ExternalMediaInliner;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
async init() {
|
|
3
3
|
const debug = require('@tryghost/debug')('mediaInliner');
|
|
4
|
-
const MediaInliner = require('
|
|
4
|
+
const MediaInliner = require('./ExternalMediaInliner');
|
|
5
5
|
const models = require('../../models');
|
|
6
6
|
const jobsService = require('../jobs');
|
|
7
7
|
|
|
@@ -5,6 +5,7 @@ const tpl = require('@tryghost/tpl');
|
|
|
5
5
|
const providers = require('./providers');
|
|
6
6
|
const parseContext = require('./parse-context');
|
|
7
7
|
const actionsMap = require('./actions-map-cache');
|
|
8
|
+
const {setIsRoles} = require('../../models/role-utils');
|
|
8
9
|
|
|
9
10
|
const messages = {
|
|
10
11
|
noPermissionToAction: 'You do not have permission to perform this action',
|
|
@@ -66,8 +67,8 @@ class CanThisResult {
|
|
|
66
67
|
|
|
67
68
|
return true;
|
|
68
69
|
};
|
|
69
|
-
|
|
70
|
-
if (
|
|
70
|
+
const {isOwner} = setIsRoles(loadedPermissions);
|
|
71
|
+
if (isOwner) {
|
|
71
72
|
hasUserPermission = true;
|
|
72
73
|
} else if (!_.isEmpty(userPermissions)) {
|
|
73
74
|
hasUserPermission = _.some(userPermissions, checkPermission);
|
|
@@ -9,6 +9,8 @@ const models = require('../../models');
|
|
|
9
9
|
// This listens to all manner of model events to find new content that needs a URL...
|
|
10
10
|
const events = require('../../lib/common/events');
|
|
11
11
|
|
|
12
|
+
/** @typedef {'posts' | 'pages' | 'tags' | 'authors'} ResourceType */
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* @description At the moment the resources class is directly responsible for data population
|
|
14
16
|
* for URLs...but because it's actually a storage cache of all published
|
|
@@ -173,7 +175,7 @@ class Resources {
|
|
|
173
175
|
* If we resolve (https://github.com/TryGhost/Ghost/issues/10360) and talk to the Content API,
|
|
174
176
|
* we could pass on e.g. `?include=authors&fields=authors.id,authors.slug`, but the API has to support it.
|
|
175
177
|
*
|
|
176
|
-
* @param {
|
|
178
|
+
* @param {import('bookshelf').Model} model
|
|
177
179
|
* @param {Object} resourceConfig
|
|
178
180
|
* @private
|
|
179
181
|
*/
|
|
@@ -227,8 +229,8 @@ class Resources {
|
|
|
227
229
|
* all subscribers sequentially. The first generator, where the conditions match the resource, will
|
|
228
230
|
* own the resource and it's url.
|
|
229
231
|
*
|
|
230
|
-
* @param {
|
|
231
|
-
* @param {
|
|
232
|
+
* @param {ResourceType} type
|
|
233
|
+
* @param {import('bookshelf').Model} model
|
|
232
234
|
* @private
|
|
233
235
|
*/
|
|
234
236
|
async _onResourceAdded(type, model) {
|
|
@@ -306,8 +308,8 @@ class Resources {
|
|
|
306
308
|
* - but the data changed and is maybe no longer owned?
|
|
307
309
|
* - e.g. featured:false changes and your filter requires featured posts
|
|
308
310
|
*
|
|
309
|
-
* @param {
|
|
310
|
-
* @param {
|
|
311
|
+
* @param {ResourceType} type
|
|
312
|
+
* @param {import('bookshelf').Model} model
|
|
311
313
|
* @private
|
|
312
314
|
*/
|
|
313
315
|
async _onResourceUpdated(type, model) {
|
|
@@ -389,37 +391,25 @@ class Resources {
|
|
|
389
391
|
|
|
390
392
|
/**
|
|
391
393
|
* @description Listener for "model removed" event.
|
|
392
|
-
* @param {
|
|
393
|
-
* @param {
|
|
394
|
+
* @param {ResourceType} type
|
|
395
|
+
* @param {import('bookshelf').Model} model
|
|
394
396
|
* @private
|
|
395
397
|
*/
|
|
396
398
|
_onResourceRemoved(type, model) {
|
|
397
399
|
debug('_onResourceRemoved', type);
|
|
398
400
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
// CASE: search for the cached resource and stop if it was found
|
|
403
|
-
this.data[type].every((_resource, _index) => {
|
|
404
|
-
if (_resource.data.id === model._previousAttributes.id) {
|
|
405
|
-
resource = _resource;
|
|
406
|
-
index = _index;
|
|
407
|
-
// break!
|
|
408
|
-
return false;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return true;
|
|
412
|
-
});
|
|
401
|
+
const resourceId = model._previousAttributes.id;
|
|
402
|
+
const index = this.data[type].findIndex(resource => resource.data.id === resourceId);
|
|
413
403
|
|
|
414
|
-
//
|
|
415
|
-
if (index ===
|
|
416
|
-
debug('
|
|
404
|
+
// Resource might not be in cache if e.g. visibility was internal
|
|
405
|
+
if (index === -1) {
|
|
406
|
+
debug('Resource not found in cache', resourceId);
|
|
417
407
|
return;
|
|
418
408
|
}
|
|
419
409
|
|
|
420
410
|
// remove the resource from cache
|
|
421
|
-
this.data[type].splice(index, 1);
|
|
422
|
-
|
|
411
|
+
const [removedResource] = this.data[type].splice(index, 1);
|
|
412
|
+
removedResource.remove();
|
|
423
413
|
}
|
|
424
414
|
|
|
425
415
|
/**
|
|
@@ -432,7 +422,7 @@ class Resources {
|
|
|
432
422
|
|
|
433
423
|
/**
|
|
434
424
|
* @description Get all cached resourced by type.
|
|
435
|
-
* @param {
|
|
425
|
+
* @param {ResourceType} type
|
|
436
426
|
* @returns {Object}
|
|
437
427
|
*/
|
|
438
428
|
getAllByType(type) {
|
|
@@ -441,8 +431,8 @@ class Resources {
|
|
|
441
431
|
|
|
442
432
|
/**
|
|
443
433
|
* @description Get all cached resourced by resource id and type.
|
|
444
|
-
* @param {
|
|
445
|
-
* @param {
|
|
434
|
+
* @param {ResourceType} type
|
|
435
|
+
* @param {string} id
|
|
446
436
|
* @returns {Object}
|
|
447
437
|
*/
|
|
448
438
|
getByIdAndType(type, id) {
|
|
@@ -54,7 +54,7 @@ class UrlService {
|
|
|
54
54
|
this.queue.addListener('started', this._onQueueStartedListener);
|
|
55
55
|
|
|
56
56
|
this._onQueueEndedListener = this._onQueueEnded.bind(this);
|
|
57
|
-
this.queue.addListener('ended', this.
|
|
57
|
+
this.queue.addListener('ended', this._onQueueEndedListener);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -263,17 +263,7 @@ class UrlService {
|
|
|
263
263
|
owns(routerId, id) {
|
|
264
264
|
debug('owns', routerId, id);
|
|
265
265
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
this.urlGenerators.every((_urlGenerator) => {
|
|
269
|
-
if (_urlGenerator.identifier === routerId) {
|
|
270
|
-
urlGenerator = _urlGenerator;
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return true;
|
|
275
|
-
});
|
|
276
|
-
|
|
266
|
+
const urlGenerator = this.urlGenerators.find(g => g.identifier === routerId);
|
|
277
267
|
if (!urlGenerator) {
|
|
278
268
|
return false;
|
|
279
269
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const _ = require('lodash');
|
|
2
1
|
const debug = require('@tryghost/debug')('services:url:urls');
|
|
3
2
|
const urlUtils = require('../../../shared/url-utils');
|
|
4
3
|
const logging = require('@tryghost/logging');
|
|
@@ -7,6 +6,10 @@ const errors = require('@tryghost/errors');
|
|
|
7
6
|
// This emits its own url added/removed events
|
|
8
7
|
const events = require('../../lib/common/events');
|
|
9
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{url: string, generatorId: string, resource: import('./Resource')}} Url
|
|
11
|
+
*/
|
|
12
|
+
|
|
10
13
|
/**
|
|
11
14
|
* This class keeps track of all urls in the system.
|
|
12
15
|
* Each resource has exactly one url. Each url is owned by exactly one url generator id.
|
|
@@ -20,24 +23,14 @@ const events = require('../../lib/common/events');
|
|
|
20
23
|
* You can easily ask `this.urls[resourceId]`.
|
|
21
24
|
*/
|
|
22
25
|
class Urls {
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
* @param {Object} [options]
|
|
26
|
-
* @param {Object} [options.urls] map of available URLs with their resources
|
|
27
|
-
*/
|
|
28
|
-
constructor({urls = {}} = {}) {
|
|
29
|
-
this.urls = urls;
|
|
30
|
-
}
|
|
26
|
+
/** @type {Object<string, Url>} */
|
|
27
|
+
urls = {};
|
|
31
28
|
|
|
32
29
|
/**
|
|
33
30
|
* @description Add a url to the system.
|
|
34
|
-
* @param {
|
|
35
|
-
* @param {import('./Resource')} options.resource - instance of the Resource class
|
|
36
|
-
* @param {string} options.generatorId
|
|
37
|
-
* @param {string} options.url
|
|
31
|
+
* @param {Url} options
|
|
38
32
|
*/
|
|
39
|
-
add(
|
|
40
|
-
const {url, generatorId, resource} = options;
|
|
33
|
+
add({url, generatorId, resource}) {
|
|
41
34
|
debug('add', resource.data.id, url);
|
|
42
35
|
|
|
43
36
|
if (this.urls[resource.data.id]) {
|
|
@@ -76,7 +69,7 @@ class Urls {
|
|
|
76
69
|
/**
|
|
77
70
|
* @description Get url by resource id.
|
|
78
71
|
* @param {String} id
|
|
79
|
-
* @returns {
|
|
72
|
+
* @returns {Url}
|
|
80
73
|
*/
|
|
81
74
|
getByResourceId(id) {
|
|
82
75
|
return this.urls[id];
|
|
@@ -85,16 +78,10 @@ class Urls {
|
|
|
85
78
|
/**
|
|
86
79
|
* @description Get all urls by generator id.
|
|
87
80
|
* @param {String} generatorId
|
|
88
|
-
* @returns {
|
|
81
|
+
* @returns {Url[]}
|
|
89
82
|
*/
|
|
90
83
|
getByGeneratorId(generatorId) {
|
|
91
|
-
return
|
|
92
|
-
if (this.urls[resourceId].generatorId === generatorId) {
|
|
93
|
-
toReturn.push(this.urls[resourceId]);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return toReturn;
|
|
97
|
-
}, []);
|
|
84
|
+
return Object.values(this.urls).filter(url => url.generatorId === generatorId);
|
|
98
85
|
}
|
|
99
86
|
|
|
100
87
|
/**
|
|
@@ -108,20 +95,17 @@ class Urls {
|
|
|
108
95
|
*
|
|
109
96
|
* But depending on the routing registration, you will always serve e.g. resource1,
|
|
110
97
|
* because the router it belongs to was registered first.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} urlToLookup
|
|
100
|
+
* @returns {Url[]}
|
|
111
101
|
*/
|
|
112
|
-
getByUrl(
|
|
113
|
-
return
|
|
114
|
-
if (this.urls[resourceId].url === url) {
|
|
115
|
-
toReturn.push(this.urls[resourceId]);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return toReturn;
|
|
119
|
-
}, []);
|
|
102
|
+
getByUrl(urlToLookup) {
|
|
103
|
+
return Object.values(this.urls).filter(url => url.url === urlToLookup);
|
|
120
104
|
}
|
|
121
105
|
|
|
122
106
|
/**
|
|
123
107
|
* @description Remove url.
|
|
124
|
-
* @param id
|
|
108
|
+
* @param {string} id
|
|
125
109
|
*/
|
|
126
110
|
removeResourceId(id) {
|
|
127
111
|
if (!this.urls[id]) {
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
},
|
|
215
215
|
"comments": {
|
|
216
216
|
"url": "https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/comments-ui.min.js",
|
|
217
|
-
"version": "1.
|
|
217
|
+
"version": "1.1"
|
|
218
218
|
},
|
|
219
219
|
"signupForm": {
|
|
220
220
|
"url": "https://cdn.jsdelivr.net/ghost/signup-form@~{version}/umd/signup-form.min.js",
|
package/core/shared/labs.js
CHANGED
|
@@ -54,7 +54,7 @@ class CacheManager {
|
|
|
54
54
|
if (!this.settingsCache) {
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
let override;
|
|
59
59
|
if (this.settingsOverrides && Object.keys(this.settingsOverrides).includes(key)) {
|
|
60
60
|
// Wrap the override value in an object in case it's a boolean
|
|
@@ -181,8 +181,8 @@ class CacheManager {
|
|
|
181
181
|
*
|
|
182
182
|
* Optionally takes a collection of settings & can populate the cache with these.
|
|
183
183
|
*
|
|
184
|
-
* @param {EventEmitter} events
|
|
185
|
-
* @param {
|
|
184
|
+
* @param {import('events').EventEmitter} events
|
|
185
|
+
* @param {import('bookshelf').Collection<import('bookshelf').Model>} settingsCollection
|
|
186
186
|
* @param {Array} calculatedFields
|
|
187
187
|
* @param {Object} cacheStore - cache storage instance base on Cache Base Adapter
|
|
188
188
|
* @param {Object} settingsOverrides - key/value pairs of settings which are overridden (i.e. via config)
|
|
@@ -219,7 +219,7 @@ class CacheManager {
|
|
|
219
219
|
|
|
220
220
|
/**
|
|
221
221
|
* Reset both the cache and the listeners, must be called during init
|
|
222
|
-
* @param {EventEmitter} events
|
|
222
|
+
* @param {import('events').EventEmitter} events
|
|
223
223
|
*/
|
|
224
224
|
reset(events) {
|
|
225
225
|
if (this.settingsCache) {
|