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.
Files changed (134) hide show
  1. package/components/{tryghost-adapter-cache-redis-5.112.0.tgz → tryghost-adapter-cache-redis-5.113.0.tgz} +0 -0
  2. package/components/{tryghost-adapter-manager-5.112.0.tgz → tryghost-adapter-manager-5.113.0.tgz} +0 -0
  3. package/components/tryghost-announcement-bar-settings-5.113.0.tgz +0 -0
  4. package/components/{tryghost-api-framework-5.112.0.tgz → tryghost-api-framework-5.113.0.tgz} +0 -0
  5. package/components/{tryghost-api-version-compatibility-service-5.112.0.tgz → tryghost-api-version-compatibility-service-5.113.0.tgz} +0 -0
  6. package/components/{tryghost-audience-feedback-5.112.0.tgz → tryghost-audience-feedback-5.113.0.tgz} +0 -0
  7. package/components/tryghost-bookshelf-repository-5.113.0.tgz +0 -0
  8. package/components/tryghost-bootstrap-socket-5.113.0.tgz +0 -0
  9. package/components/tryghost-captcha-service-5.113.0.tgz +0 -0
  10. package/components/tryghost-constants-5.113.0.tgz +0 -0
  11. package/components/tryghost-custom-fonts-5.113.0.tgz +0 -0
  12. package/components/tryghost-custom-theme-settings-service-5.113.0.tgz +0 -0
  13. package/components/{tryghost-data-generator-5.112.0.tgz → tryghost-data-generator-5.113.0.tgz} +0 -0
  14. package/components/tryghost-domain-events-5.113.0.tgz +0 -0
  15. package/components/{tryghost-donations-5.112.0.tgz → tryghost-donations-5.113.0.tgz} +0 -0
  16. package/components/tryghost-email-addresses-5.113.0.tgz +0 -0
  17. package/components/{tryghost-email-analytics-provider-mailgun-5.112.0.tgz → tryghost-email-analytics-provider-mailgun-5.113.0.tgz} +0 -0
  18. package/components/tryghost-email-analytics-service-5.113.0.tgz +0 -0
  19. package/components/{tryghost-email-content-generator-5.112.0.tgz → tryghost-email-content-generator-5.113.0.tgz} +0 -0
  20. package/components/tryghost-email-events-5.113.0.tgz +0 -0
  21. package/components/{tryghost-email-service-5.112.0.tgz → tryghost-email-service-5.113.0.tgz} +0 -0
  22. package/components/{tryghost-email-suppression-list-5.112.0.tgz → tryghost-email-suppression-list-5.113.0.tgz} +0 -0
  23. package/components/{tryghost-express-dynamic-redirects-5.112.0.tgz → tryghost-express-dynamic-redirects-5.113.0.tgz} +0 -0
  24. package/components/tryghost-extract-api-key-5.113.0.tgz +0 -0
  25. package/components/{tryghost-ghost-5.112.0.tgz → tryghost-ghost-5.113.0.tgz} +0 -0
  26. package/components/{tryghost-html-to-plaintext-5.112.0.tgz → tryghost-html-to-plaintext-5.113.0.tgz} +0 -0
  27. package/components/tryghost-i18n-5.113.0.tgz +0 -0
  28. package/components/{tryghost-identity-token-service-5.112.0.tgz → tryghost-identity-token-service-5.113.0.tgz} +0 -0
  29. package/components/{tryghost-importer-handler-content-files-5.112.0.tgz → tryghost-importer-handler-content-files-5.113.0.tgz} +0 -0
  30. package/components/{tryghost-importer-revue-5.112.0.tgz → tryghost-importer-revue-5.113.0.tgz} +0 -0
  31. package/components/{tryghost-in-memory-repository-5.112.0.tgz → tryghost-in-memory-repository-5.113.0.tgz} +0 -0
  32. package/components/tryghost-job-manager-5.113.0.tgz +0 -0
  33. package/components/{tryghost-link-redirects-5.112.0.tgz → tryghost-link-redirects-5.113.0.tgz} +0 -0
  34. package/components/{tryghost-link-replacer-5.112.0.tgz → tryghost-link-replacer-5.113.0.tgz} +0 -0
  35. package/components/{tryghost-magic-link-5.112.0.tgz → tryghost-magic-link-5.113.0.tgz} +0 -0
  36. package/components/{tryghost-mail-events-5.112.0.tgz → tryghost-mail-events-5.113.0.tgz} +0 -0
  37. package/components/tryghost-mailgun-client-5.113.0.tgz +0 -0
  38. package/components/tryghost-member-attribution-5.113.0.tgz +0 -0
  39. package/components/tryghost-member-events-5.113.0.tgz +0 -0
  40. package/components/{tryghost-members-api-5.112.0.tgz → tryghost-members-api-5.113.0.tgz} +0 -0
  41. package/components/tryghost-members-csv-5.113.0.tgz +0 -0
  42. package/components/{tryghost-members-importer-5.112.0.tgz → tryghost-members-importer-5.113.0.tgz} +0 -0
  43. package/components/{tryghost-members-offers-5.112.0.tgz → tryghost-members-offers-5.113.0.tgz} +0 -0
  44. package/components/tryghost-members-payments-5.113.0.tgz +0 -0
  45. package/components/{tryghost-members-ssr-5.112.0.tgz → tryghost-members-ssr-5.113.0.tgz} +0 -0
  46. package/components/{tryghost-members-stripe-service-5.112.0.tgz → tryghost-members-stripe-service-5.113.0.tgz} +0 -0
  47. package/components/tryghost-milestones-5.113.0.tgz +0 -0
  48. package/components/{tryghost-minifier-5.112.0.tgz → tryghost-minifier-5.113.0.tgz} +0 -0
  49. package/components/{tryghost-mw-api-version-mismatch-5.112.0.tgz → tryghost-mw-api-version-mismatch-5.113.0.tgz} +0 -0
  50. package/components/tryghost-mw-cache-control-5.113.0.tgz +0 -0
  51. package/components/{tryghost-mw-error-handler-5.112.0.tgz → tryghost-mw-error-handler-5.113.0.tgz} +0 -0
  52. package/components/{tryghost-mw-session-from-token-5.112.0.tgz → tryghost-mw-session-from-token-5.113.0.tgz} +0 -0
  53. package/components/{tryghost-mw-update-user-last-seen-5.112.0.tgz → tryghost-mw-update-user-last-seen-5.113.0.tgz} +0 -0
  54. package/components/tryghost-mw-version-match-5.113.0.tgz +0 -0
  55. package/components/tryghost-mw-vhost-5.113.0.tgz +0 -0
  56. package/components/{tryghost-package-json-5.112.0.tgz → tryghost-package-json-5.113.0.tgz} +0 -0
  57. package/components/{tryghost-post-events-5.112.0.tgz → tryghost-post-events-5.113.0.tgz} +0 -0
  58. package/components/tryghost-post-revisions-5.113.0.tgz +0 -0
  59. package/components/{tryghost-posts-service-5.112.0.tgz → tryghost-posts-service-5.113.0.tgz} +0 -0
  60. package/components/{tryghost-prometheus-metrics-5.112.0.tgz → tryghost-prometheus-metrics-5.113.0.tgz} +0 -0
  61. package/components/tryghost-recommendations-5.113.0.tgz +0 -0
  62. package/components/tryghost-referrers-5.113.0.tgz +0 -0
  63. package/components/{tryghost-security-5.112.0.tgz → tryghost-security-5.113.0.tgz} +0 -0
  64. package/components/tryghost-session-service-5.113.0.tgz +0 -0
  65. package/components/{tryghost-settings-path-manager-5.112.0.tgz → tryghost-settings-path-manager-5.113.0.tgz} +0 -0
  66. package/components/{tryghost-slack-notifications-5.112.0.tgz → tryghost-slack-notifications-5.113.0.tgz} +0 -0
  67. package/components/tryghost-tiers-5.113.0.tgz +0 -0
  68. package/components/{tryghost-version-notifications-data-service-5.112.0.tgz → tryghost-version-notifications-data-service-5.113.0.tgz} +0 -0
  69. package/components/{tryghost-webmentions-5.112.0.tgz → tryghost-webmentions-5.113.0.tgz} +0 -0
  70. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +12953 -12243
  71. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-ad8698fe.mjs → CodeEditorView-ed5e87be.mjs} +2 -2
  72. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  73. package/core/built/admin/assets/admin-x-settings/{index-463cec50.mjs → index-0ee4d13c.mjs} +2 -2
  74. package/core/built/admin/assets/admin-x-settings/{index-2713e469.mjs → index-9c7da716.mjs} +19975 -19965
  75. package/core/built/admin/assets/admin-x-settings/{modals-033e8fc4.mjs → modals-7708d510.mjs} +2226 -2215
  76. package/core/built/admin/assets/{chunk.524.db49da6fd8ae155205a4.js → chunk.524.4f0aeb6b611079e528f5.js} +7 -7
  77. package/core/built/admin/assets/{chunk.582.0bf715eb6807f7641706.js → chunk.582.485df00698ed27a0668b.js} +9 -9
  78. package/core/built/admin/assets/{ghost-62bd4d4c837d453e1038808dc1cd1e4c.js → ghost-ebf07ae7768b6e9fb9a4b173b6917782.js} +66 -66
  79. package/core/built/admin/assets/posts/posts.js +1 -1
  80. package/core/built/admin/assets/{vendor-fca15534b8426c0567400113c63a3e21.js → vendor-68a4aa424a179a90f5bbc2b750def576.js} +28 -26
  81. package/core/built/admin/index.html +4 -4
  82. package/core/frontend/services/routing/registry.js +6 -6
  83. package/core/frontend/src/admin-auth/message-handler.js +1 -1
  84. package/core/server/adapters/cache/AdapterCacheMemoryTTL.js +54 -0
  85. package/core/server/adapters/cache/memory-ttl.js +1 -1
  86. package/core/server/data/migrations/versions/5.113/2025-03-07-12-24-00-add-super-editor.js +31 -0
  87. package/core/server/data/migrations/versions/5.113/2025-03-07-12-25-00-add-member-perms-to-super-editor.js +291 -0
  88. package/core/server/data/schema/fixtures/fixtures.json +27 -0
  89. package/core/server/models/invite.js +2 -2
  90. package/core/server/models/role.js +2 -2
  91. package/core/server/models/user.js +39 -28
  92. package/core/server/services/email-analytics/jobs/update-member-email-analytics/index.js +13 -0
  93. package/core/server/services/email-analytics/lib/queries.js +3 -3
  94. package/core/server/services/media-inliner/ExternalMediaInliner.js +346 -0
  95. package/core/server/services/media-inliner/service.js +1 -1
  96. package/core/server/services/permissions/can-this.js +3 -2
  97. package/core/server/services/url/Resources.js +19 -29
  98. package/core/server/services/url/UrlService.js +2 -12
  99. package/core/server/services/url/Urls.js +17 -33
  100. package/core/shared/config/defaults.json +1 -1
  101. package/core/shared/labs.js +2 -1
  102. package/core/shared/settings-cache/CacheManager.js +4 -4
  103. package/package.json +134 -134
  104. package/yarn.lock +10 -10
  105. package/components/tryghost-adapter-cache-memory-ttl-5.112.0.tgz +0 -0
  106. package/components/tryghost-announcement-bar-settings-5.112.0.tgz +0 -0
  107. package/components/tryghost-bookshelf-repository-5.112.0.tgz +0 -0
  108. package/components/tryghost-bootstrap-socket-5.112.0.tgz +0 -0
  109. package/components/tryghost-captcha-service-5.112.0.tgz +0 -0
  110. package/components/tryghost-constants-5.112.0.tgz +0 -0
  111. package/components/tryghost-custom-fonts-5.112.0.tgz +0 -0
  112. package/components/tryghost-custom-theme-settings-service-5.112.0.tgz +0 -0
  113. package/components/tryghost-domain-events-5.112.0.tgz +0 -0
  114. package/components/tryghost-email-addresses-5.112.0.tgz +0 -0
  115. package/components/tryghost-email-analytics-service-5.112.0.tgz +0 -0
  116. package/components/tryghost-email-events-5.112.0.tgz +0 -0
  117. package/components/tryghost-external-media-inliner-5.112.0.tgz +0 -0
  118. package/components/tryghost-extract-api-key-5.112.0.tgz +0 -0
  119. package/components/tryghost-i18n-5.112.0.tgz +0 -0
  120. package/components/tryghost-job-manager-5.112.0.tgz +0 -0
  121. package/components/tryghost-mailgun-client-5.112.0.tgz +0 -0
  122. package/components/tryghost-member-attribution-5.112.0.tgz +0 -0
  123. package/components/tryghost-member-events-5.112.0.tgz +0 -0
  124. package/components/tryghost-members-csv-5.112.0.tgz +0 -0
  125. package/components/tryghost-members-payments-5.112.0.tgz +0 -0
  126. package/components/tryghost-milestones-5.112.0.tgz +0 -0
  127. package/components/tryghost-mw-cache-control-5.112.0.tgz +0 -0
  128. package/components/tryghost-mw-version-match-5.112.0.tgz +0 -0
  129. package/components/tryghost-mw-vhost-5.112.0.tgz +0 -0
  130. package/components/tryghost-post-revisions-5.112.0.tgz +0 -0
  131. package/components/tryghost-recommendations-5.112.0.tgz +0 -0
  132. package/components/tryghost-referrers-5.112.0.tgz +0 -0
  133. package/components/tryghost-session-service-5.112.0.tgz +0 -0
  134. 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 {'completed'|'started'} field - The field to update.
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 === 'completed' ? 'finished_at' : 'started_at';
114
- const status = field === 'completed' ? 'finished' : 'started';
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?))| |<|\\\\|&quot;|$)`;
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('@tryghost/external-media-inliner');
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 (loadedPermissions.user && _.some(loadedPermissions.user.roles, {name: 'Owner'})) {
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 {Bookshelf-Model} model
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 {String} type (post,user...)
231
- * @param {Bookshelf-Model} model
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 {String} type (post,user...)
310
- * @param {Bookshelf-Model} model
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 {String} type (post,user...)
393
- * @param {Bookshelf-Model} model
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
- let index = null;
400
- let resource;
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
- // CASE: there are possible cases that the resource was not fetched e.g. visibility is internal
415
- if (index === null) {
416
- debug('can\'t find resource', model._previousAttributes.id);
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
- resource.remove();
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 {String} type (post, user...)
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 {String} type (post, user...)
445
- * @param {String} id
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._onQueueEnded.bind(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
- let urlGenerator;
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 {Object} options
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(options) {
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 {Object}
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 {Array}
81
+ * @returns {Url[]}
89
82
  */
90
83
  getByGeneratorId(generatorId) {
91
- return _.reduce(Object.keys(this.urls), (toReturn, resourceId) => {
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(url) {
113
- return _.reduce(Object.keys(this.urls), (toReturn, resourceId) => {
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.0"
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",
@@ -38,7 +38,8 @@ const BETA_FEATURES = [
38
38
  'ActivityPub',
39
39
  'importMemberTier',
40
40
  'staff2fa',
41
- 'contentVisibility'
41
+ 'contentVisibility',
42
+ 'superEditors'
42
43
  ];
43
44
 
44
45
  const ALPHA_FEATURES = [
@@ -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 {Bookshelf.Collection<Settings>} settingsCollection
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) {