ghost 5.36.1 → 5.38.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 (208) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.38.0.tgz +0 -0
  2. package/components/tryghost-adapter-cache-redis-5.38.0.tgz +0 -0
  3. package/components/tryghost-adapter-manager-5.38.0.tgz +0 -0
  4. package/components/tryghost-api-framework-5.38.0.tgz +0 -0
  5. package/components/tryghost-api-version-compatibility-service-5.38.0.tgz +0 -0
  6. package/components/tryghost-audience-feedback-5.38.0.tgz +0 -0
  7. package/components/tryghost-bootstrap-socket-5.38.0.tgz +0 -0
  8. package/components/tryghost-constants-5.38.0.tgz +0 -0
  9. package/components/tryghost-custom-theme-settings-service-5.38.0.tgz +0 -0
  10. package/components/tryghost-data-generator-5.38.0.tgz +0 -0
  11. package/components/tryghost-domain-events-5.38.0.tgz +0 -0
  12. package/components/tryghost-dynamic-routing-events-5.38.0.tgz +0 -0
  13. package/components/tryghost-email-analytics-provider-mailgun-5.38.0.tgz +0 -0
  14. package/components/tryghost-email-analytics-service-5.38.0.tgz +0 -0
  15. package/components/{tryghost-email-content-generator-5.36.1.tgz → tryghost-email-content-generator-5.38.0.tgz} +0 -0
  16. package/components/tryghost-email-events-5.38.0.tgz +0 -0
  17. package/components/tryghost-email-service-5.38.0.tgz +0 -0
  18. package/components/tryghost-email-suppression-list-5.38.0.tgz +0 -0
  19. package/components/tryghost-event-aware-cache-wrapper-5.38.0.tgz +0 -0
  20. package/components/{tryghost-express-dynamic-redirects-5.36.1.tgz → tryghost-express-dynamic-redirects-5.38.0.tgz} +0 -0
  21. package/components/tryghost-external-media-inliner-5.38.0.tgz +0 -0
  22. package/components/tryghost-extract-api-key-5.38.0.tgz +0 -0
  23. package/components/tryghost-html-to-plaintext-5.38.0.tgz +0 -0
  24. package/components/tryghost-i18n-5.38.0.tgz +0 -0
  25. package/components/tryghost-importer-handler-content-files-5.38.0.tgz +0 -0
  26. package/components/tryghost-importer-revue-5.38.0.tgz +0 -0
  27. package/components/tryghost-job-manager-5.38.0.tgz +0 -0
  28. package/components/tryghost-link-redirects-5.38.0.tgz +0 -0
  29. package/components/tryghost-link-replacer-5.38.0.tgz +0 -0
  30. package/components/tryghost-link-tracking-5.38.0.tgz +0 -0
  31. package/components/tryghost-magic-link-5.38.0.tgz +0 -0
  32. package/components/tryghost-mailgun-client-5.38.0.tgz +0 -0
  33. package/components/tryghost-member-attribution-5.38.0.tgz +0 -0
  34. package/components/tryghost-member-events-5.38.0.tgz +0 -0
  35. package/components/tryghost-members-api-5.38.0.tgz +0 -0
  36. package/components/tryghost-members-csv-5.38.0.tgz +0 -0
  37. package/components/{tryghost-members-events-service-5.36.1.tgz → tryghost-members-events-service-5.38.0.tgz} +0 -0
  38. package/components/tryghost-members-importer-5.38.0.tgz +0 -0
  39. package/components/tryghost-members-offers-5.38.0.tgz +0 -0
  40. package/components/tryghost-members-payments-5.38.0.tgz +0 -0
  41. package/components/tryghost-members-ssr-5.38.0.tgz +0 -0
  42. package/components/tryghost-members-stripe-service-5.38.0.tgz +0 -0
  43. package/components/tryghost-milestones-5.38.0.tgz +0 -0
  44. package/components/tryghost-minifier-5.38.0.tgz +0 -0
  45. package/components/tryghost-mw-api-version-mismatch-5.38.0.tgz +0 -0
  46. package/components/tryghost-mw-cache-control-5.38.0.tgz +0 -0
  47. package/components/tryghost-mw-error-handler-5.38.0.tgz +0 -0
  48. package/components/tryghost-mw-session-from-token-5.38.0.tgz +0 -0
  49. package/components/tryghost-mw-update-user-last-seen-5.38.0.tgz +0 -0
  50. package/components/tryghost-mw-version-match-5.38.0.tgz +0 -0
  51. package/components/tryghost-mw-vhost-5.38.0.tgz +0 -0
  52. package/components/tryghost-oembed-service-5.38.0.tgz +0 -0
  53. package/components/tryghost-package-json-5.38.0.tgz +0 -0
  54. package/components/tryghost-referrers-5.38.0.tgz +0 -0
  55. package/components/tryghost-security-5.38.0.tgz +0 -0
  56. package/components/{tryghost-session-service-5.36.1.tgz → tryghost-session-service-5.38.0.tgz} +0 -0
  57. package/components/tryghost-settings-path-manager-5.38.0.tgz +0 -0
  58. package/components/tryghost-slack-notifications-5.38.0.tgz +0 -0
  59. package/components/tryghost-staff-service-5.38.0.tgz +0 -0
  60. package/components/tryghost-stats-service-5.38.0.tgz +0 -0
  61. package/components/tryghost-tiers-5.38.0.tgz +0 -0
  62. package/components/tryghost-update-check-service-5.38.0.tgz +0 -0
  63. package/components/tryghost-verification-trigger-5.38.0.tgz +0 -0
  64. package/components/tryghost-version-notifications-data-service-5.38.0.tgz +0 -0
  65. package/components/tryghost-webmentions-5.38.0.tgz +0 -0
  66. package/content/themes/casper/assets/built/screen.css +1 -1
  67. package/content/themes/casper/assets/built/screen.css.map +1 -1
  68. package/content/themes/casper/assets/css/screen.css +44 -30
  69. package/content/themes/casper/default.hbs +2 -2
  70. package/content/themes/casper/error.hbs +2 -2
  71. package/content/themes/casper/package.json +1 -1
  72. package/core/boot.js +15 -6
  73. package/core/built/admin/assets/chunk.143.c6802c882a911797ce4f.js +49 -0
  74. package/core/built/admin/assets/chunk.178.09faefd4027fcba4113d.js +10 -0
  75. package/core/built/admin/assets/{chunk.502.800e1515996bcc900013.js → chunk.220.9ca2950240aba3fced21.js} +2168 -2035
  76. package/core/built/admin/assets/{chunk.79.53e8aa9671b2d5dae8ba.js → chunk.79.acb7dd01e1c785f4920c.js} +191 -183
  77. package/core/built/admin/assets/{ghost-b828e9e3c161aae92909c2e163656bb1.js → ghost-35103ff053c43f1dfa7f35821c3c2412.js} +271 -249
  78. package/core/built/admin/assets/ghost-a9307c9cfe26a4bc621e02cd3bae421a.css +1 -0
  79. package/core/built/admin/assets/ghost-dark-f309cf445255344e4861a95ecb8f1920.css +1 -0
  80. package/core/built/admin/assets/{vendor-c4684647d4f5213e5dbb6763de430e7e.js → vendor-b982e3bf1020bff77b2a3c44d5f59e55.js} +442 -457
  81. package/core/built/admin/index.html +6 -6
  82. package/core/frontend/apps/amp/lib/helpers/amp_content.js +1 -5
  83. package/core/frontend/helpers/ghost_head.js +4 -1
  84. package/core/frontend/meta/asset-url.js +9 -0
  85. package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
  86. package/core/frontend/services/sitemap/base-generator.js +5 -1
  87. package/core/server/adapters/storage/LocalImagesStorage.js +1 -1
  88. package/core/server/api/endpoints/db.js +17 -0
  89. package/core/server/api/endpoints/email-previews.js +2 -43
  90. package/core/server/api/endpoints/emails.js +1 -22
  91. package/core/server/api/endpoints/mentions.js +2 -1
  92. package/core/server/api/endpoints/utils/serializers/output/mappers/emails.js +14 -8
  93. package/core/server/api/endpoints/utils/serializers/output/posts.js +2 -2
  94. package/core/server/data/db/backup.js +13 -13
  95. package/core/server/data/importer/handlers/image.js +2 -2
  96. package/core/server/data/importer/import-manager.js +64 -5
  97. package/core/server/data/importer/importers/ContentFileImporter.js +128 -0
  98. package/core/server/data/migrations/versions/4.9/05-fix-missed-mobiledoc-url-transforms.js +1 -1
  99. package/core/server/data/schema/commands.js +21 -10
  100. package/core/server/lib/common/events.js +16 -23
  101. package/core/server/models/base/plugins/relations.js +5 -3
  102. package/core/server/models/index.js +5 -0
  103. package/core/server/models/mention.js +13 -0
  104. package/core/server/run-update-check.js +3 -1
  105. package/core/server/services/comments/emails.js +2 -2
  106. package/core/server/services/email-service/wrapper.js +2 -0
  107. package/core/server/services/link-tracking/LinkClickRepository.js +1 -1
  108. package/core/server/services/media-inliner/index.js +1 -0
  109. package/core/server/services/media-inliner/service.js +62 -0
  110. package/core/server/services/members/stats/members-stats.js +13 -9
  111. package/core/server/services/mentions/BookshelfMentionRepository.js +12 -1
  112. package/core/server/services/mentions/MentionController.js +7 -1
  113. package/core/server/services/mentions/WebmentionMetadata.js +3 -2
  114. package/core/server/services/mentions/service.js +5 -3
  115. package/core/server/services/posts/posts-service.js +3 -14
  116. package/core/server/{analytics-events.js → services/segment/index.js} +4 -3
  117. package/core/server/services/staff/index.js +2 -0
  118. package/core/server/services/stripe/config.js +4 -0
  119. package/core/server/services/url/Urls.js +10 -2
  120. package/core/server/update-check.js +5 -3
  121. package/core/server/web/api/endpoints/admin/app.js +5 -4
  122. package/core/server/web/api/endpoints/admin/routes.js +6 -0
  123. package/core/server/web/api/middleware/index.js +1 -2
  124. package/core/shared/config/overrides.json +34 -0
  125. package/core/shared/labs.js +3 -3
  126. package/package.json +164 -159
  127. package/yarn.lock +887 -934
  128. package/components/tryghost-adapter-cache-memory-ttl-5.36.1.tgz +0 -0
  129. package/components/tryghost-adapter-cache-redis-5.36.1.tgz +0 -0
  130. package/components/tryghost-adapter-manager-5.36.1.tgz +0 -0
  131. package/components/tryghost-api-framework-5.36.1.tgz +0 -0
  132. package/components/tryghost-api-version-compatibility-service-5.36.1.tgz +0 -0
  133. package/components/tryghost-audience-feedback-5.36.1.tgz +0 -0
  134. package/components/tryghost-bootstrap-socket-5.36.1.tgz +0 -0
  135. package/components/tryghost-constants-5.36.1.tgz +0 -0
  136. package/components/tryghost-custom-theme-settings-service-5.36.1.tgz +0 -0
  137. package/components/tryghost-data-generator-5.36.1.tgz +0 -0
  138. package/components/tryghost-domain-events-5.36.1.tgz +0 -0
  139. package/components/tryghost-dynamic-routing-events-5.36.1.tgz +0 -0
  140. package/components/tryghost-email-analytics-provider-mailgun-5.36.1.tgz +0 -0
  141. package/components/tryghost-email-analytics-service-5.36.1.tgz +0 -0
  142. package/components/tryghost-email-events-5.36.1.tgz +0 -0
  143. package/components/tryghost-email-service-5.36.1.tgz +0 -0
  144. package/components/tryghost-email-suppression-list-5.36.1.tgz +0 -0
  145. package/components/tryghost-event-aware-cache-wrapper-5.36.1.tgz +0 -0
  146. package/components/tryghost-extract-api-key-5.36.1.tgz +0 -0
  147. package/components/tryghost-html-to-plaintext-5.36.1.tgz +0 -0
  148. package/components/tryghost-i18n-5.36.1.tgz +0 -0
  149. package/components/tryghost-importer-revue-5.36.1.tgz +0 -0
  150. package/components/tryghost-job-manager-5.36.1.tgz +0 -0
  151. package/components/tryghost-link-redirects-5.36.1.tgz +0 -0
  152. package/components/tryghost-link-replacer-5.36.1.tgz +0 -0
  153. package/components/tryghost-link-tracking-5.36.1.tgz +0 -0
  154. package/components/tryghost-magic-link-5.36.1.tgz +0 -0
  155. package/components/tryghost-mailgun-client-5.36.1.tgz +0 -0
  156. package/components/tryghost-member-attribution-5.36.1.tgz +0 -0
  157. package/components/tryghost-member-events-5.36.1.tgz +0 -0
  158. package/components/tryghost-members-api-5.36.1.tgz +0 -0
  159. package/components/tryghost-members-csv-5.36.1.tgz +0 -0
  160. package/components/tryghost-members-importer-5.36.1.tgz +0 -0
  161. package/components/tryghost-members-offers-5.36.1.tgz +0 -0
  162. package/components/tryghost-members-payments-5.36.1.tgz +0 -0
  163. package/components/tryghost-members-ssr-5.36.1.tgz +0 -0
  164. package/components/tryghost-members-stripe-service-5.36.1.tgz +0 -0
  165. package/components/tryghost-milestones-5.36.1.tgz +0 -0
  166. package/components/tryghost-minifier-5.36.1.tgz +0 -0
  167. package/components/tryghost-mw-api-version-mismatch-5.36.1.tgz +0 -0
  168. package/components/tryghost-mw-cache-control-5.36.1.tgz +0 -0
  169. package/components/tryghost-mw-error-handler-5.36.1.tgz +0 -0
  170. package/components/tryghost-mw-session-from-token-5.36.1.tgz +0 -0
  171. package/components/tryghost-mw-update-user-last-seen-5.36.1.tgz +0 -0
  172. package/components/tryghost-mw-vhost-5.36.1.tgz +0 -0
  173. package/components/tryghost-oembed-service-5.36.1.tgz +0 -0
  174. package/components/tryghost-package-json-5.36.1.tgz +0 -0
  175. package/components/tryghost-referrers-5.36.1.tgz +0 -0
  176. package/components/tryghost-security-5.36.1.tgz +0 -0
  177. package/components/tryghost-settings-path-manager-5.36.1.tgz +0 -0
  178. package/components/tryghost-slack-notifications-5.36.1.tgz +0 -0
  179. package/components/tryghost-staff-service-5.36.1.tgz +0 -0
  180. package/components/tryghost-stats-service-5.36.1.tgz +0 -0
  181. package/components/tryghost-tiers-5.36.1.tgz +0 -0
  182. package/components/tryghost-update-check-service-5.36.1.tgz +0 -0
  183. package/components/tryghost-verification-trigger-5.36.1.tgz +0 -0
  184. package/components/tryghost-version-notifications-data-service-5.36.1.tgz +0 -0
  185. package/components/tryghost-webmentions-5.36.1.tgz +0 -0
  186. package/core/built/admin/assets/chunk.143.26ea9f26571d656653f0.js +0 -49
  187. package/core/built/admin/assets/chunk.178.dd71b3a764b73facc400.js +0 -11
  188. package/core/built/admin/assets/ghost-7ecf5c7934d90798485ee5ac2956f7fe.css +0 -1
  189. package/core/built/admin/assets/ghost-dark-e50717df8e57d3e7fee67a0bcea895ad.css +0 -1
  190. package/core/frontend/src/cards/css/before-after.css +0 -81
  191. package/core/frontend/src/cards/js/before-after.js +0 -36
  192. package/core/server/data/importer/importers/image.js +0 -76
  193. package/core/server/data/schema/clients/index.js +0 -7
  194. package/core/server/data/schema/clients/mysql.js +0 -34
  195. package/core/server/data/schema/clients/sqlite3.js +0 -39
  196. package/core/server/services/bulk-email/bulk-email-processor.js +0 -289
  197. package/core/server/services/bulk-email/index.js +0 -1
  198. package/core/server/services/mega/email-preview.js +0 -54
  199. package/core/server/services/mega/feedback-buttons.js +0 -66
  200. package/core/server/services/mega/index.js +0 -14
  201. package/core/server/services/mega/mega.js +0 -626
  202. package/core/server/services/mega/post-email-serializer.js +0 -559
  203. package/core/server/services/mega/segment-parser.js +0 -20
  204. package/core/server/services/mega/template.js +0 -1319
  205. package/core/server/services/mentions/WebmentionRequest.js +0 -20
  206. package/core/server/web/admin/middleware.js +0 -17
  207. package/core/server/web/api/middleware/version-match.js +0 -31
  208. /package/core/built/admin/assets/{chunk.502.800e1515996bcc900013.js.LICENSE.txt → chunk.220.9ca2950240aba3fced21.js.LICENSE.txt} +0 -0
@@ -6,7 +6,6 @@ const tpl = require('@tryghost/tpl');
6
6
  const db = require('../db');
7
7
  const DatabaseInfo = require('@tryghost/database-info');
8
8
  const schema = require('./schema');
9
- const clients = require('./clients');
10
9
 
11
10
  const messages = {
12
11
  hasPrimaryKeySQLiteError: 'Must use hasPrimaryKeySQLite on an SQLite3 database',
@@ -432,11 +431,15 @@ function deleteTable(table, transaction = db.knex) {
432
431
  /**
433
432
  * @param {import('knex').Knex} [transaction] - connection to the DB
434
433
  */
435
- function getTables(transaction = db.knex) {
434
+ async function getTables(transaction = db.knex) {
436
435
  const client = transaction.client.config.client;
437
436
 
438
- if (_.includes(_.keys(clients), client)) {
439
- return clients[client].getTables(transaction);
437
+ if (client === 'sqlite3') {
438
+ const response = await transaction.raw('select * from sqlite_master where type = "table"');
439
+ return _.reject(_.map(response, 'tbl_name'), name => name === 'sqlite_sequence');
440
+ } else if (client === 'mysql2') {
441
+ const response = await transaction.raw('show tables');
442
+ return _.flatten(_.map(response[0], entry => _.values(entry)));
440
443
  }
441
444
 
442
445
  return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
@@ -446,11 +449,15 @@ function getTables(transaction = db.knex) {
446
449
  * @param {string} table
447
450
  * @param {import('knex').Knex} [transaction] - connection to the DB
448
451
  */
449
- function getIndexes(table, transaction = db.knex) {
452
+ async function getIndexes(table, transaction = db.knex) {
450
453
  const client = transaction.client.config.client;
451
454
 
452
- if (_.includes(_.keys(clients), client)) {
453
- return clients[client].getIndexes(table, transaction);
455
+ if (client === 'sqlite3') {
456
+ const response = await transaction.raw(`pragma index_list("${table}")`);
457
+ return _.flatten(_.map(response, 'name'));
458
+ } else if (client === 'mysql2') {
459
+ const response = await transaction.raw(`SHOW INDEXES from ${table}`);
460
+ return _.flatten(_.map(response[0], 'Key_name'));
454
461
  }
455
462
 
456
463
  return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
@@ -460,11 +467,15 @@ function getIndexes(table, transaction = db.knex) {
460
467
  * @param {string} table
461
468
  * @param {import('knex').Knex} [transaction] - connection to the DB
462
469
  */
463
- function getColumns(table, transaction = db.knex) {
470
+ async function getColumns(table, transaction = db.knex) {
464
471
  const client = transaction.client.config.client;
465
472
 
466
- if (_.includes(_.keys(clients), client)) {
467
- return clients[client].getColumns(table);
473
+ if (client === 'sqlite3') {
474
+ const response = await transaction.raw(`pragma table_info("${table}")`);
475
+ return _.flatten(_.map(response, 'name'));
476
+ } else if (client === 'mysql2') {
477
+ const response = await transaction.raw(`SHOW COLUMNS from ${table}`);
478
+ return _.flatten(_.map(response[0], 'Field'));
468
479
  }
469
480
 
470
481
  return Promise.reject(tpl(messages.noSupportForDatabase, {client: client}));
@@ -12,29 +12,22 @@
12
12
  */
13
13
 
14
14
  const events = require('events');
15
- const util = require('util');
16
- let EventRegistry;
17
- let EventRegistryInstance;
18
15
 
19
- EventRegistry = function () {
20
- events.EventEmitter.call(this);
21
- };
16
+ class EventRegistry extends events.EventEmitter {
17
+ /**
18
+ * This is method is semi-hack to make sure listeners are only registered once
19
+ * during the lifetime of the process. And example problem it solves is
20
+ * registering duplicate listeners between Ghost instance reboots when running tests.
21
+ * @param {String} eventName
22
+ * @param {String} listenerName named function name registered as a listener for the event
23
+ * @returns {Boolean}
24
+ */
25
+ hasRegisteredListener(eventName, listenerName) {
26
+ return !!(this.listeners(eventName).find(listener => (listener.name === listenerName)));
27
+ }
28
+ }
22
29
 
23
- util.inherits(EventRegistry, events.EventEmitter);
30
+ const eventRegistryInstance = new EventRegistry();
31
+ eventRegistryInstance.setMaxListeners(100);
24
32
 
25
- /**
26
- * This is method is semi-hack to make sure listeners are only registered once
27
- * during the lifetime of the process. And example problem it solves is
28
- * registering duplicate listeners between Ghost instance reboots when running tests.
29
- * @param {String} eventName
30
- * @param {String} listenerName named function name registered as a listener for the event
31
- * @returns {Boolean}
32
- */
33
- EventRegistry.prototype.hasRegisteredListener = function (eventName, listenerName) {
34
- return !!(this.listeners(eventName).find(listener => (listener.name === listenerName)));
35
- };
36
-
37
- EventRegistryInstance = new EventRegistry();
38
- EventRegistryInstance.setMaxListeners(100);
39
-
40
- module.exports = EventRegistryInstance;
33
+ module.exports = eventRegistryInstance;
@@ -7,7 +7,7 @@ module.exports = function (Bookshelf) {
7
7
  * Return a relation, and load it if it hasn't been loaded already (or force a refresh with the forceRefresh option).
8
8
  * refs https://github.com/TryGhost/Team/issues/1626
9
9
  * @param {string} name Name of the relation to load
10
- * @param {Object} [options] Options to pass to the fetch when not yet loaded (or when force refreshing)
10
+ * @param {Object} [options] Options to pass to the fetch when not yet loaded (or when force refreshing)
11
11
  * @param {boolean} [options.forceRefresh] If true, the relation will be fetched again even if it has already been loaded.
12
12
  * @returns {Promise<import('bookshelf').Model|import('bookshelf').Collection|null>}
13
13
  */
@@ -23,8 +23,10 @@ module.exports = function (Bookshelf) {
23
23
  // Not yet loaded, or force refresh
24
24
  // Note that we don't use .refresh on the relation on options.forceRefresh
25
25
  // Because the relation can also be a collection, which doesn't have a refresh method
26
- this.relations[name] = this[name]();
27
- return this.relations[name].fetch(options);
26
+ const instance = this[name]();
27
+ await instance.fetch(options);
28
+ this.relations[name] = instance;
29
+ return instance;
28
30
  }
29
31
  });
30
32
  };
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  const _ = require('lodash');
6
+ const debug = require('@tryghost/debug')('models');
6
7
  const glob = require('glob');
7
8
 
8
9
  // enable event listeners
@@ -14,12 +15,16 @@ require('./base/listeners');
14
15
  exports = module.exports;
15
16
 
16
17
  function init() {
18
+ const baseNow = Date.now();
17
19
  exports.Base = require('./base');
20
+ debug(`${Date.now() - baseNow}ms - Base.js require`);
18
21
 
19
22
  let modelsFiles = glob.sync('!(index).js', {cwd: __dirname});
20
23
  modelsFiles.forEach((model) => {
21
24
  const name = model.replace(/.js$/, '');
25
+ const modelNow = Date.now();
22
26
  _.extend(exports, require('./' + name));
27
+ debug(`${Date.now() - modelNow}ms - ${model} require`);
23
28
  });
24
29
  }
25
30
 
@@ -9,6 +9,19 @@ const Mention = ghostBookshelf.Model.extend({
9
9
  enforcedFilters() {
10
10
  return 'deleted:false';
11
11
  }
12
+ }, {
13
+ permittedOptions(methodName) {
14
+ let options = ghostBookshelf.Model.permittedOptions.call(this, methodName);
15
+ const validOptions = {
16
+ findPage: ['selectRaw', 'whereRaw']
17
+ };
18
+
19
+ if (validOptions[methodName]) {
20
+ options = options.concat(validOptions[methodName]);
21
+ }
22
+
23
+ return options;
24
+ }
12
25
  });
13
26
 
14
27
  module.exports = {
@@ -42,7 +42,9 @@ if (parentPort) {
42
42
  await settings.init();
43
43
  // Finished INIT
44
44
 
45
- await updateCheck();
45
+ await updateCheck({
46
+ rethrowErrors: true
47
+ });
46
48
 
47
49
  postParentPortMessage(`Ran update check`);
48
50
 
@@ -2,7 +2,7 @@ const {promises: fs} = require('fs');
2
2
  const path = require('path');
3
3
  const moment = require('moment');
4
4
  const htmlToPlaintext = require('@tryghost/html-to-plaintext');
5
- const postEmailSerializer = require('../mega/post-email-serializer');
5
+ const emailService = require('../email-service');
6
6
 
7
7
  class CommentsServiceEmails {
8
8
  constructor({config, logging, models, mailer, settingsCache, settingsHelpers, urlService, urlUtils}) {
@@ -95,7 +95,7 @@ class CommentsServiceEmails {
95
95
  accentColor: this.settingsCache.get('accent_color'),
96
96
  fromEmail: this.notificationFromAddress,
97
97
  toEmail: to,
98
- profileUrl: postEmailSerializer.createUnsubscribeUrl(member.get('uuid'), {comments: true})
98
+ profileUrl: emailService.renderer.createUnsubscribeUrl(member.get('uuid'), {comments: true})
99
99
  };
100
100
 
101
101
  const {html, text} = await this.renderEmailTemplate('new-comment-reply', templateData);
@@ -94,6 +94,8 @@ class EmailServiceWrapper {
94
94
  sentry
95
95
  });
96
96
 
97
+ this.renderer = emailRenderer;
98
+
97
99
  this.service = new EmailService({
98
100
  batchSendingService,
99
101
  sendingService,
@@ -56,7 +56,7 @@ module.exports = class LinkClickRepository {
56
56
  }
57
57
 
58
58
  const model = await this.#MemberLinkClickEventModel.add({
59
- // Only store the parthname (no support for variable query strings)
59
+ // Only store the pathname (no support for variable query strings)
60
60
  redirect_id: linkClick.link_id.toHexString(),
61
61
  member_id: member.id
62
62
  }, {});
@@ -0,0 +1 @@
1
+ module.exports = require('./service');
@@ -0,0 +1,62 @@
1
+ module.exports = {
2
+ async init() {
3
+ const debug = require('@tryghost/debug')('mediaInliner');
4
+ const MediaInliner = require('@tryghost/external-media-inliner');
5
+ const models = require('../../models');
6
+ const jobsService = require('../jobs');
7
+
8
+ const mediaStorage = require('../../adapters/storage').getStorage('media');
9
+ const imageStorage = require('../../adapters/storage').getStorage('images');
10
+ const fileStorage = require('../../adapters/storage').getStorage('files');
11
+
12
+ const config = require('../../../shared/config');
13
+
14
+ const mediaInliner = new MediaInliner({
15
+ PostModel: models.Post,
16
+ TagModel: models.Tag,
17
+ UserModel: models.User,
18
+ PostMetaModel: models.PostsMeta,
19
+ getMediaStorage: (extension) => {
20
+ if (config.get('uploads').images.extensions.includes(extension)) {
21
+ return imageStorage;
22
+ } else if (config.get('uploads').media.extensions.includes(extension)) {
23
+ return mediaStorage;
24
+ } else if (config.get('uploads').files.extensions.includes(extension)) {
25
+ return fileStorage;
26
+ } else {
27
+ return null;
28
+ }
29
+ }
30
+ });
31
+
32
+ this.api = {
33
+
34
+ startMediaInliner: async (domains) => {
35
+ if (!domains || !domains.length) {
36
+ // default domains to inline from if none are provided
37
+ domains = [
38
+ 'https://s3.amazonaws.com/revue',
39
+ 'https://substackcdn.com'
40
+ ];
41
+ }
42
+
43
+ debug('[Inliner] Starting media inlining job for domains: ', domains);
44
+
45
+ // @NOTE: the job is "inline" (aka non-offloaded into a thread), because usecases are currently
46
+ // limited to migrational, so there is no expectations for site's availability etc.
47
+ await jobsService.addJob({
48
+ name: 'external-media-inliner',
49
+ job: (data) => {
50
+ return mediaInliner.inline(data.domains);
51
+ },
52
+ data: {domains},
53
+ offloaded: false
54
+ });
55
+
56
+ return {
57
+ status: 'success'
58
+ };
59
+ }
60
+ };
61
+ }
62
+ };
@@ -1,5 +1,4 @@
1
1
  const moment = require('moment-timezone');
2
- const Promise = require('bluebird');
3
2
 
4
3
  const dateFormat = 'YYYY-MM-DD HH:mm:ss';
5
4
  class MembersStats {
@@ -131,14 +130,19 @@ class MembersStats {
131
130
  const totalMembers = await this.getTotalMembers();
132
131
 
133
132
  // perform final calculations in parallel
134
- const results = await Promise.props({
135
- total: totalMembers,
136
- total_in_range: this.getTotalMembersInRange({days, totalMembers, siteTimezone}),
137
- total_on_date: this.getTotalMembersOnDatesInRange({days, totalMembers, siteTimezone}),
138
- new_today: this.getNewMembersToday({siteTimezone})
139
- });
140
-
141
- return results;
133
+ const [total, totalInRange, totalOnDate, newToday] = await Promise.all([
134
+ totalMembers,
135
+ this.getTotalMembersInRange({days, totalMembers, siteTimezone}),
136
+ this.getTotalMembersOnDatesInRange({days, totalMembers, siteTimezone}),
137
+ this.getNewMembersToday({siteTimezone})
138
+ ]);
139
+
140
+ return {
141
+ total,
142
+ total_in_range: totalInRange,
143
+ total_on_date: totalOnDate,
144
+ new_today: newToday
145
+ };
142
146
  }
143
147
  }
144
148
 
@@ -65,7 +65,18 @@ module.exports = class BookshelfMentionRepository {
65
65
  * @returns {Promise<Page<import('@tryghost/webmentions/lib/Mention')>>}
66
66
  */
67
67
  async getPage(options) {
68
- const page = await this.#MentionModel.findPage(options);
68
+ /**
69
+ * @type {GetPageOptions & {whereRaw?: string}}
70
+ */
71
+ const _options = {
72
+ ...options
73
+ };
74
+ delete _options.unique;
75
+ if (options.unique) {
76
+ _options.whereRaw = 'NOT EXISTS (select id from mentions as m where m.id > mentions.id and m.source = mentions.source)';
77
+ }
78
+
79
+ const page = await this.#MentionModel.findPage(_options);
69
80
 
70
81
  return {
71
82
  data: await Promise.all(page.data.map(model => this.#modelToMention(model))),
@@ -80,11 +80,17 @@ module.exports = class MentionController {
80
80
  order = 'created_at asc';
81
81
  }
82
82
 
83
+ let unique;
84
+ if (frame.options.unique && (frame.options.unique === 'true' || frame.options.unique === true)) {
85
+ unique = true;
86
+ }
87
+
83
88
  const mentions = await this.#api.listMentions({
84
89
  filter: frame.options.filter,
85
90
  order,
86
91
  limit,
87
- page
92
+ page,
93
+ unique
88
94
  });
89
95
 
90
96
  const resources = await Promise.all(mentions.data.map((mention) => {
@@ -6,14 +6,15 @@ module.exports = class WebmentionMetadata {
6
6
  * @returns {Promise<import('@tryghost/webmentions/lib/MentionsAPI').WebmentionMetadata>}
7
7
  */
8
8
  async fetch(url) {
9
- const data = await oembedService.fetchOembedDataFromUrl(url.href, 'bookmark');
9
+ const data = await oembedService.fetchOembedDataFromUrl(url.href, 'mention');
10
10
  const result = {
11
11
  siteTitle: data.metadata.publisher,
12
12
  title: data.metadata.title,
13
13
  excerpt: data.metadata.description,
14
14
  author: data.metadata.author,
15
15
  image: data.metadata.thumbnail ? new URL(data.metadata.thumbnail) : null,
16
- favicon: data.metadata.icon ? new URL(data.metadata.icon) : null
16
+ favicon: data.metadata.icon ? new URL(data.metadata.icon) : null,
17
+ body: data.body
17
18
  };
18
19
  return result;
19
20
  }
@@ -1,6 +1,5 @@
1
1
  const MentionController = require('./MentionController');
2
2
  const WebmentionMetadata = require('./WebmentionMetadata');
3
- const WebmentionRequest = require('./WebmentionRequest');
4
3
  const {
5
4
  MentionsAPI,
6
5
  MentionSendingService,
@@ -27,13 +26,17 @@ function getPostUrl(post) {
27
26
 
28
27
  module.exports = {
29
28
  controller: new MentionController(),
29
+ didInit: false,
30
30
  async init() {
31
+ if (this.didInit) {
32
+ return;
33
+ }
34
+ this.didInit = true;
31
35
  const repository = new BookshelfMentionRepository({
32
36
  MentionModel: models.Mention,
33
37
  DomainEvents
34
38
  });
35
39
  const webmentionMetadata = new WebmentionMetadata();
36
- const webmentionRequest = new WebmentionRequest();
37
40
  const discoveryService = new MentionDiscoveryService({externalRequest});
38
41
  const resourceService = new ResourceService({
39
42
  urlUtils,
@@ -49,7 +52,6 @@ module.exports = {
49
52
  const api = new MentionsAPI({
50
53
  repository,
51
54
  webmentionMetadata,
52
- webmentionRequest,
53
55
  resourceService,
54
56
  routingService
55
57
  });
@@ -8,8 +8,7 @@ const messages = {
8
8
  };
9
9
 
10
10
  class PostsService {
11
- constructor({mega, urlUtils, models, isSet, stats, emailService}) {
12
- this.mega = mega;
11
+ constructor({urlUtils, models, isSet, stats, emailService}) {
13
12
  this.urlUtils = urlUtils;
14
13
  this.models = models;
15
14
  this.isSet = isSet;
@@ -45,17 +44,9 @@ class PostsService {
45
44
  let email;
46
45
 
47
46
  if (!postEmail) {
48
- if (this.isSet('emailStability')) {
49
- email = await this.emailService.createEmail(model);
50
- } else {
51
- email = await this.mega.addEmail(model, frame.options);
52
- }
47
+ email = await this.emailService.createEmail(model);
53
48
  } else if (postEmail && postEmail.get('status') === 'failed') {
54
- if (this.isSet('emailStability')) {
55
- email = await this.emailService.retryEmail(postEmail);
56
- } else {
57
- email = await this.mega.retryFailedEmail(postEmail);
58
- }
49
+ email = await this.emailService.retryEmail(postEmail);
59
50
  }
60
51
  if (email) {
61
52
  model.set('email', email);
@@ -130,7 +121,6 @@ class PostsService {
130
121
  */
131
122
  const getPostServiceInstance = () => {
132
123
  const urlUtils = require('../../../shared/url-utils');
133
- const {mega} = require('../mega');
134
124
  const labs = require('../../../shared/labs');
135
125
  const models = require('../../models');
136
126
  const PostStats = require('./stats/post-stats');
@@ -139,7 +129,6 @@ const getPostServiceInstance = () => {
139
129
  const postStats = new PostStats();
140
130
 
141
131
  return new PostsService({
142
- mega: mega,
143
132
  urlUtils: urlUtils,
144
133
  models: models,
145
134
  isSet: flag => labs.isSet(flag), // don't use bind, that breaks test subbing of labs
@@ -1,11 +1,12 @@
1
1
  const _ = require('lodash');
2
2
  const Analytics = require('analytics-node');
3
- const config = require('../shared/config');
4
3
  const logging = require('@tryghost/logging');
5
- const sentry = require('../shared/sentry');
4
+
5
+ const config = require('../../../shared/config');
6
+ const sentry = require('../../../shared/sentry');
6
7
 
7
8
  // Listens to model events to layer on analytics - also uses the "fake" theme.uploaded event from the theme API
8
- const events = require('./lib/common/events');
9
+ const events = require('../../lib/common/events');
9
10
 
10
11
  module.exports.init = function () {
11
12
  const analytics = new Analytics(config.get('segment:key'));
@@ -12,6 +12,7 @@ class StaffServiceWrapper {
12
12
 
13
13
  const logging = require('@tryghost/logging');
14
14
  const models = require('../../models');
15
+ const memberAttribution = require('../member-attribution');
15
16
  const {GhostMailer} = require('../mail');
16
17
  const mailer = new GhostMailer();
17
18
  const settingsCache = require('../../../shared/settings-cache');
@@ -26,6 +27,7 @@ class StaffServiceWrapper {
26
27
  settingsCache,
27
28
  urlUtils,
28
29
  DomainEvents,
30
+ memberAttributionService: memberAttribution.service,
29
31
  labs
30
32
  });
31
33
 
@@ -1,5 +1,6 @@
1
1
  const logging = require('@tryghost/logging');
2
2
  const tpl = require('@tryghost/tpl');
3
+ const labs = require('../../../shared/labs');
3
4
 
4
5
  const messages = {
5
6
  remoteWebhooksInDevelopment: 'Cannot use remote webhooks in development. See https://ghost.org/docs/webhooks/#stripe-webhooks for developing with Stripe.'
@@ -64,6 +65,9 @@ module.exports = {
64
65
  ...keys,
65
66
  ...urls,
66
67
  enablePromoCodes: config.get('enableStripePromoCodes'),
68
+ get enableAutomaticTax() {
69
+ return labs.isSet('stripeAutomaticTax');
70
+ },
67
71
  webhookSecret: webhookSecret,
68
72
  webhookHandlerUrl: webhookHandlerUrl.href
69
73
  };
@@ -44,10 +44,18 @@ class Urls {
44
44
  debug('cache', url);
45
45
 
46
46
  if (this.urls[resource.data.id]) {
47
- logging.error(new errors.InternalServerError({
47
+ const error = new errors.InternalServerError({
48
48
  message: 'This should not happen.',
49
49
  code: 'URLSERVICE_RESOURCE_DUPLICATE'
50
- }));
50
+ });
51
+ if (process.env.NODE_ENV.startsWith('test')) {
52
+ logging.warn({
53
+ message: 'Duplicate URL',
54
+ err: error
55
+ });
56
+ } else {
57
+ logging.error(error);
58
+ }
51
59
 
52
60
  this.removeResourceId(resource.data.id);
53
61
  }
@@ -12,10 +12,11 @@ const UpdateCheckService = require('@tryghost/update-check-service');
12
12
 
13
13
  /**
14
14
  * Initializes and triggers update check
15
- *
15
+ * @param {Object} [options]
16
+ * @param {Boolean} [options.rethrowErrors] - if true, errors will be thrown instead of logged
16
17
  * @returns {Promise<any>}
17
18
  */
18
- module.exports = async () => {
19
+ module.exports = async ({rethrowErrors = false} = {}) => {
19
20
  const allowedCheckEnvironments = ['development', 'production'];
20
21
 
21
22
  // CASE: The check will not happen if your NODE_ENV is not in the allowed defined environments.
@@ -51,7 +52,8 @@ module.exports = async () => {
51
52
  notificationGroups: config.get('notificationGroups'),
52
53
  siteUrl: urlUtils.urlFor('home', true),
53
54
  forceUpdate: config.get('updateCheck:forceUpdate'),
54
- ghostVersion: ghostVersion.original
55
+ ghostVersion: ghostVersion.original,
56
+ rethrowErrors
55
57
  },
56
58
  request,
57
59
  sendEmail: ghostMailer.send.bind(ghostMailer)
@@ -1,10 +1,11 @@
1
1
  const debug = require('@tryghost/debug')('web:endpoints:admin:app');
2
2
  const boolParser = require('express-query-boolean');
3
- const express = require('../../../../../shared/express');
4
3
  const bodyParser = require('body-parser');
5
- const shared = require('../../../shared');
6
- const apiMw = require('../../middleware');
7
4
  const errorHandler = require('@tryghost/mw-error-handler');
5
+ const versionMatch = require('@tryghost/mw-version-match');
6
+
7
+ const shared = require('../../../shared');
8
+ const express = require('../../../../../shared/express');
8
9
  const sentry = require('../../../../../shared/sentry');
9
10
  const routes = require('./routes');
10
11
  const APIVersionCompatibilityService = require('../../../../services/api-version-compatibility');
@@ -24,7 +25,7 @@ module.exports = function setupApiApp() {
24
25
 
25
26
  // Check version matches for API requests, depends on res.locals.safeVersion being set
26
27
  // Therefore must come after themeHandler.ghostLocals, for now
27
- apiApp.use(apiMw.versionMatch);
28
+ apiApp.use(versionMatch);
28
29
 
29
30
  // Admin API shouldn't be cached
30
31
  apiApp.use(shared.middleware.cacheControl('private'));
@@ -205,6 +205,12 @@ module.exports = function apiRoutes() {
205
205
  http(api.db.backupContent)
206
206
  );
207
207
 
208
+ router.post('/db/media/inline',
209
+ mw.authAdminApi,
210
+ labs.enabledMiddleware('mediaInliner'),
211
+ http(api.db.inlineMedia)
212
+ );
213
+
208
214
  // ## Slack
209
215
  router.post('/slack/test', mw.authAdminApi, http(api.slack.sendTest));
210
216
 
@@ -1,6 +1,5 @@
1
1
  module.exports = {
2
2
  cors: require('./cors'),
3
3
  updateUserLastSeen: require('./update-user-last-seen'),
4
- upload: require('./upload'),
5
- versionMatch: require('./version-match')
4
+ upload: require('./upload')
6
5
  };