ghost 5.74.2 → 5.74.4

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 (162) hide show
  1. package/components/{tryghost-adapter-cache-memory-ttl-5.74.2.tgz → tryghost-adapter-cache-memory-ttl-5.74.4.tgz} +0 -0
  2. package/components/{tryghost-adapter-cache-redis-5.74.2.tgz → tryghost-adapter-cache-redis-5.74.4.tgz} +0 -0
  3. package/components/{tryghost-adapter-manager-5.74.2.tgz → tryghost-adapter-manager-5.74.4.tgz} +0 -0
  4. package/components/{tryghost-announcement-bar-settings-5.74.2.tgz → tryghost-announcement-bar-settings-5.74.4.tgz} +0 -0
  5. package/components/{tryghost-api-framework-5.74.2.tgz → tryghost-api-framework-5.74.4.tgz} +0 -0
  6. package/components/tryghost-api-version-compatibility-service-5.74.4.tgz +0 -0
  7. package/components/{tryghost-audience-feedback-5.74.2.tgz → tryghost-audience-feedback-5.74.4.tgz} +0 -0
  8. package/components/tryghost-bookshelf-repository-5.74.4.tgz +0 -0
  9. package/components/tryghost-bootstrap-socket-5.74.4.tgz +0 -0
  10. package/components/tryghost-collections-5.74.4.tgz +0 -0
  11. package/components/{tryghost-constants-5.74.2.tgz → tryghost-constants-5.74.4.tgz} +0 -0
  12. package/components/tryghost-custom-theme-settings-service-5.74.4.tgz +0 -0
  13. package/components/{tryghost-data-generator-5.74.2.tgz → tryghost-data-generator-5.74.4.tgz} +0 -0
  14. package/components/tryghost-domain-events-5.74.4.tgz +0 -0
  15. package/components/tryghost-donations-5.74.4.tgz +0 -0
  16. package/components/tryghost-dynamic-routing-events-5.74.4.tgz +0 -0
  17. package/components/tryghost-email-addresses-5.74.4.tgz +0 -0
  18. package/components/{tryghost-email-analytics-provider-mailgun-5.74.2.tgz → tryghost-email-analytics-provider-mailgun-5.74.4.tgz} +0 -0
  19. package/components/tryghost-email-analytics-service-5.74.4.tgz +0 -0
  20. package/components/{tryghost-email-content-generator-5.74.2.tgz → tryghost-email-content-generator-5.74.4.tgz} +0 -0
  21. package/components/{tryghost-email-events-5.74.2.tgz → tryghost-email-events-5.74.4.tgz} +0 -0
  22. package/components/tryghost-email-service-5.74.4.tgz +0 -0
  23. package/components/tryghost-email-suppression-list-5.74.4.tgz +0 -0
  24. package/components/{tryghost-event-aware-cache-wrapper-5.74.2.tgz → tryghost-event-aware-cache-wrapper-5.74.4.tgz} +0 -0
  25. package/components/{tryghost-express-dynamic-redirects-5.74.2.tgz → tryghost-express-dynamic-redirects-5.74.4.tgz} +0 -0
  26. package/components/{tryghost-external-media-inliner-5.74.2.tgz → tryghost-external-media-inliner-5.74.4.tgz} +0 -0
  27. package/components/tryghost-extract-api-key-5.74.4.tgz +0 -0
  28. package/components/{tryghost-html-to-plaintext-5.74.2.tgz → tryghost-html-to-plaintext-5.74.4.tgz} +0 -0
  29. package/components/tryghost-i18n-5.74.4.tgz +0 -0
  30. package/components/tryghost-importer-handler-content-files-5.74.4.tgz +0 -0
  31. package/components/{tryghost-importer-revue-5.74.2.tgz → tryghost-importer-revue-5.74.4.tgz} +0 -0
  32. package/components/tryghost-in-memory-repository-5.74.4.tgz +0 -0
  33. package/components/{tryghost-job-manager-5.74.2.tgz → tryghost-job-manager-5.74.4.tgz} +0 -0
  34. package/components/{tryghost-link-redirects-5.74.2.tgz → tryghost-link-redirects-5.74.4.tgz} +0 -0
  35. package/components/tryghost-link-replacer-5.74.4.tgz +0 -0
  36. package/components/{tryghost-link-tracking-5.74.2.tgz → tryghost-link-tracking-5.74.4.tgz} +0 -0
  37. package/components/{tryghost-magic-link-5.74.2.tgz → tryghost-magic-link-5.74.4.tgz} +0 -0
  38. package/components/tryghost-mail-events-5.74.4.tgz +0 -0
  39. package/components/{tryghost-mailgun-client-5.74.2.tgz → tryghost-mailgun-client-5.74.4.tgz} +0 -0
  40. package/components/tryghost-member-attribution-5.74.4.tgz +0 -0
  41. package/components/{tryghost-member-events-5.74.2.tgz → tryghost-member-events-5.74.4.tgz} +0 -0
  42. package/components/{tryghost-members-api-5.74.2.tgz → tryghost-members-api-5.74.4.tgz} +0 -0
  43. package/components/tryghost-members-csv-5.74.4.tgz +0 -0
  44. package/components/tryghost-members-events-service-5.74.4.tgz +0 -0
  45. package/components/{tryghost-members-importer-5.74.2.tgz → tryghost-members-importer-5.74.4.tgz} +0 -0
  46. package/components/tryghost-members-offers-5.74.4.tgz +0 -0
  47. package/components/tryghost-members-payments-5.74.4.tgz +0 -0
  48. package/components/{tryghost-members-ssr-5.74.2.tgz → tryghost-members-ssr-5.74.4.tgz} +0 -0
  49. package/components/{tryghost-members-stripe-service-5.74.2.tgz → tryghost-members-stripe-service-5.74.4.tgz} +0 -0
  50. package/components/{tryghost-mentions-email-report-5.74.2.tgz → tryghost-mentions-email-report-5.74.4.tgz} +0 -0
  51. package/components/{tryghost-milestones-5.74.2.tgz → tryghost-milestones-5.74.4.tgz} +0 -0
  52. package/components/tryghost-minifier-5.74.4.tgz +0 -0
  53. package/components/tryghost-model-to-domain-event-interceptor-5.74.4.tgz +0 -0
  54. package/components/tryghost-mw-api-version-mismatch-5.74.4.tgz +0 -0
  55. package/components/tryghost-mw-cache-control-5.74.4.tgz +0 -0
  56. package/components/{tryghost-mw-error-handler-5.74.2.tgz → tryghost-mw-error-handler-5.74.4.tgz} +0 -0
  57. package/components/tryghost-mw-session-from-token-5.74.4.tgz +0 -0
  58. package/components/tryghost-mw-update-user-last-seen-5.74.4.tgz +0 -0
  59. package/components/tryghost-mw-version-match-5.74.4.tgz +0 -0
  60. package/components/tryghost-mw-vhost-5.74.4.tgz +0 -0
  61. package/components/tryghost-nql-filter-expansions-5.74.4.tgz +0 -0
  62. package/components/tryghost-oembed-service-5.74.4.tgz +0 -0
  63. package/components/{tryghost-package-json-5.74.2.tgz → tryghost-package-json-5.74.4.tgz} +0 -0
  64. package/components/{tryghost-post-events-5.74.2.tgz → tryghost-post-events-5.74.4.tgz} +0 -0
  65. package/components/tryghost-post-revisions-5.74.4.tgz +0 -0
  66. package/components/{tryghost-posts-service-5.74.2.tgz → tryghost-posts-service-5.74.4.tgz} +0 -0
  67. package/components/tryghost-recommendations-5.74.4.tgz +0 -0
  68. package/components/{tryghost-referrers-5.74.2.tgz → tryghost-referrers-5.74.4.tgz} +0 -0
  69. package/components/{tryghost-security-5.74.2.tgz → tryghost-security-5.74.4.tgz} +0 -0
  70. package/components/tryghost-session-service-5.74.4.tgz +0 -0
  71. package/components/{tryghost-settings-path-manager-5.74.2.tgz → tryghost-settings-path-manager-5.74.4.tgz} +0 -0
  72. package/components/{tryghost-slack-notifications-5.74.2.tgz → tryghost-slack-notifications-5.74.4.tgz} +0 -0
  73. package/components/tryghost-staff-service-5.74.4.tgz +0 -0
  74. package/components/tryghost-stats-service-5.74.4.tgz +0 -0
  75. package/components/{tryghost-tiers-5.74.2.tgz → tryghost-tiers-5.74.4.tgz} +0 -0
  76. package/components/{tryghost-update-check-service-5.74.2.tgz → tryghost-update-check-service-5.74.4.tgz} +0 -0
  77. package/components/tryghost-verification-trigger-5.74.4.tgz +0 -0
  78. package/components/tryghost-version-notifications-data-service-5.74.4.tgz +0 -0
  79. package/components/{tryghost-webmentions-5.74.2.tgz → tryghost-webmentions-5.74.4.tgz} +0 -0
  80. package/core/boot.js +4 -0
  81. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +6 -0
  82. package/core/built/admin/assets/admin-x-demo/index-2fa5a3c2.mjs +8291 -0
  83. package/core/built/admin/assets/admin-x-demo/modals-10ef986a.mjs +381 -0
  84. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-f1193dcf.mjs → CodeEditorView-b41ea5f8.mjs} +76 -76
  85. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  86. package/core/built/admin/assets/admin-x-settings/{index-114d5aab.mjs → index-bc03188e.mjs} +2 -2
  87. package/core/built/admin/assets/admin-x-settings/{index-89bf7b3f.mjs → index-e5bc288c.mjs} +8140 -7624
  88. package/core/built/admin/assets/admin-x-settings/{modals-045fb940.mjs → modals-98e015fa.mjs} +8005 -8080
  89. package/core/built/admin/assets/{chunk.761.5cfbe0d0f8ec7aefe7bc.js → chunk.137.0297fba124bc9ba1bbc1.js} +691 -686
  90. package/core/built/admin/assets/{chunk.143.a97e9374518a7e548084.js → chunk.143.0c31227118654150d49c.js} +5 -5
  91. package/core/built/admin/assets/{chunk.178.cce4259fa74902c8483a.js → chunk.178.b2467b9ba07dbbc9016a.js} +4 -4
  92. package/core/built/admin/assets/ghost-0456ea5c74bd1b27239c11706d1acae9.css +1 -0
  93. package/core/built/admin/assets/{ghost-ad9952b86c1d886c49a92339adf702e3.js → ghost-7d0f91cd17901ac15ce0ae64a3091e2a.js} +225 -194
  94. package/core/built/admin/assets/ghost-dark-29863e517e1917a3b0df74979a6cf03f.css +1 -0
  95. package/core/built/admin/assets/koenig-lexical/index.css +1 -1
  96. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +7878 -7857
  97. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +119 -119
  98. package/core/built/admin/index.html +5 -5
  99. package/core/frontend/helpers/img_url.js +7 -97
  100. package/core/frontend/meta/image-dimensions.js +34 -6
  101. package/core/frontend/utils/images.js +100 -0
  102. package/core/server/api/endpoints/users.js +34 -5
  103. package/core/server/data/schema/schema.js +1 -1
  104. package/core/server/models/base/plugins/events.js +20 -1
  105. package/core/server/services/email-address/EmailAddressServiceWrapper.js +39 -0
  106. package/core/server/services/email-address/index.js +3 -0
  107. package/core/server/services/email-service/EmailServiceWrapper.js +2 -0
  108. package/core/server/services/mail/GhostMailer.js +46 -9
  109. package/core/server/services/members/service.js +8 -1
  110. package/core/server/services/newsletters/NewslettersService.js +52 -19
  111. package/core/server/services/newsletters/index.js +3 -1
  112. package/core/server/services/offers/OfferBookshelfRepository.js +8 -1
  113. package/core/server/services/settings/settings-service.js +4 -0
  114. package/core/server/services/settings-helpers/SettingsHelpers.js +71 -2
  115. package/core/server/services/settings-helpers/index.js +2 -1
  116. package/core/shared/config/overrides.json +2 -1
  117. package/core/shared/labs.js +5 -1
  118. package/core/shared/sentry.js +56 -33
  119. package/package.json +150 -149
  120. package/yarn.lock +700 -790
  121. package/components/tryghost-api-version-compatibility-service-5.74.2.tgz +0 -0
  122. package/components/tryghost-bookshelf-repository-5.74.2.tgz +0 -0
  123. package/components/tryghost-bootstrap-socket-5.74.2.tgz +0 -0
  124. package/components/tryghost-collections-5.74.2.tgz +0 -0
  125. package/components/tryghost-custom-theme-settings-service-5.74.2.tgz +0 -0
  126. package/components/tryghost-domain-events-5.74.2.tgz +0 -0
  127. package/components/tryghost-donations-5.74.2.tgz +0 -0
  128. package/components/tryghost-dynamic-routing-events-5.74.2.tgz +0 -0
  129. package/components/tryghost-email-analytics-service-5.74.2.tgz +0 -0
  130. package/components/tryghost-email-service-5.74.2.tgz +0 -0
  131. package/components/tryghost-email-suppression-list-5.74.2.tgz +0 -0
  132. package/components/tryghost-extract-api-key-5.74.2.tgz +0 -0
  133. package/components/tryghost-i18n-5.74.2.tgz +0 -0
  134. package/components/tryghost-importer-handler-content-files-5.74.2.tgz +0 -0
  135. package/components/tryghost-in-memory-repository-5.74.2.tgz +0 -0
  136. package/components/tryghost-link-replacer-5.74.2.tgz +0 -0
  137. package/components/tryghost-mail-events-5.74.2.tgz +0 -0
  138. package/components/tryghost-member-attribution-5.74.2.tgz +0 -0
  139. package/components/tryghost-members-csv-5.74.2.tgz +0 -0
  140. package/components/tryghost-members-events-service-5.74.2.tgz +0 -0
  141. package/components/tryghost-members-offers-5.74.2.tgz +0 -0
  142. package/components/tryghost-members-payments-5.74.2.tgz +0 -0
  143. package/components/tryghost-minifier-5.74.2.tgz +0 -0
  144. package/components/tryghost-model-to-domain-event-interceptor-5.74.2.tgz +0 -0
  145. package/components/tryghost-mw-api-version-mismatch-5.74.2.tgz +0 -0
  146. package/components/tryghost-mw-cache-control-5.74.2.tgz +0 -0
  147. package/components/tryghost-mw-session-from-token-5.74.2.tgz +0 -0
  148. package/components/tryghost-mw-update-user-last-seen-5.74.2.tgz +0 -0
  149. package/components/tryghost-mw-version-match-5.74.2.tgz +0 -0
  150. package/components/tryghost-mw-vhost-5.74.2.tgz +0 -0
  151. package/components/tryghost-nql-filter-expansions-5.74.2.tgz +0 -0
  152. package/components/tryghost-oembed-service-5.74.2.tgz +0 -0
  153. package/components/tryghost-post-revisions-5.74.2.tgz +0 -0
  154. package/components/tryghost-recommendations-5.74.2.tgz +0 -0
  155. package/components/tryghost-session-service-5.74.2.tgz +0 -0
  156. package/components/tryghost-staff-service-5.74.2.tgz +0 -0
  157. package/components/tryghost-stats-service-5.74.2.tgz +0 -0
  158. package/components/tryghost-verification-trigger-5.74.2.tgz +0 -0
  159. package/components/tryghost-version-notifications-data-service-5.74.2.tgz +0 -0
  160. package/core/built/admin/assets/ghost-ad224505f7a80c242cc1272d1755995d.css +0 -1
  161. package/core/built/admin/assets/ghost-dark-81e3ee175ed67abaafca3fa228c620a3.css +0 -1
  162. /package/core/built/admin/assets/{chunk.761.5cfbe0d0f8ec7aefe7bc.js.LICENSE.txt → chunk.137.0297fba124bc9ba1bbc1.js.LICENSE.txt} +0 -0
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.74%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22ember-websockets%22%3A%7B%22socketIO%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%2270a47c7b40%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%2247f97526c1%22%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%225.74%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22ember-websockets%22%3A%7B%22socketIO%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%22a5d777fdf2%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%2254a22f23e1%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%22f76ec8c678%22%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -37,7 +37,7 @@
37
37
  </style>
38
38
 
39
39
  <link integrity="" rel="stylesheet" href="assets/vendor-0ede59da8efb5e28fa929557f7ff7154.css">
40
- <link integrity="" rel="stylesheet" href="assets/ghost-ad224505f7a80c242cc1272d1755995d.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-0456ea5c74bd1b27239c11706d1acae9.css" title="light">
41
41
 
42
42
 
43
43
  </head>
@@ -57,8 +57,8 @@
57
57
  <div id="ember-basic-dropdown-wormhole"></div>
58
58
 
59
59
  <script src="assets/vendor-f6856af9fcd8ab6533f2a4339ececbc8.js"></script>
60
- <script src="assets/chunk.761.5cfbe0d0f8ec7aefe7bc.js"></script>
61
- <script src="assets/chunk.143.a97e9374518a7e548084.js"></script>
62
- <script src="assets/ghost-ad9952b86c1d886c49a92339adf702e3.js"></script>
60
+ <script src="assets/chunk.137.0297fba124bc9ba1bbc1.js"></script>
61
+ <script src="assets/chunk.143.0c31227118654150d49c.js"></script>
62
+ <script src="assets/ghost-7d0f91cd17901ac15ce0ae64a3091e2a.js"></script>
63
63
  </body>
64
64
  </html>
@@ -7,19 +7,21 @@
7
7
  // Returns the URL for the current object scope i.e. If inside a post scope will return image permalink
8
8
  // `absolute` flag outputs absolute URL, else URL is relative.
9
9
  const {urlUtils} = require('../services/proxy');
10
+ const {
11
+ detectInternalImage,
12
+ getImageWithSize,
13
+ getUnsplashImage,
14
+ detectUnsplashImage
15
+ } = require('../utils/images');
10
16
 
11
- const url = require('url');
12
17
  const _ = require('lodash');
13
18
  const logging = require('@tryghost/logging');
14
19
  const tpl = require('@tryghost/tpl');
15
- const imageTransform = require('@tryghost/image-transform');
16
20
 
17
21
  const messages = {
18
22
  attrIsRequired: 'Attribute is required e.g. {{img_url feature_image}}'
19
23
  };
20
24
 
21
- const STATIC_IMAGE_URL_PREFIX = `${urlUtils.STATIC_IMAGE_URL_PREFIX}`;
22
-
23
25
  module.exports = function imgUrl(requestedImageUrl, options) {
24
26
  // CASE: if no url is passed, e.g. `{{img_url}}` we show a warning
25
27
  if (arguments.length < 2) {
@@ -46,7 +48,7 @@ module.exports = function imgUrl(requestedImageUrl, options) {
46
48
 
47
49
  if (!isInternalImage) {
48
50
  // Detect Unsplash width and format
49
- const isUnsplashImage = /images\.unsplash\.com/.test(requestedImageUrl);
51
+ const isUnsplashImage = detectUnsplashImage(requestedImageUrl);
50
52
  if (isUnsplashImage) {
51
53
  try {
52
54
  return getUnsplashImage(requestedImageUrl, sizeOptions);
@@ -99,95 +101,3 @@ function getImageSizeOptions(options) {
99
101
  requestedFormat
100
102
  };
101
103
  }
102
-
103
- function detectInternalImage(requestedImageUrl) {
104
- const siteUrl = urlUtils.getSiteUrl();
105
- const isAbsoluteImage = /https?:\/\//.test(requestedImageUrl);
106
- const isAbsoluteInternalImage = isAbsoluteImage && requestedImageUrl.startsWith(siteUrl);
107
-
108
- // CASE: imagePath is a "protocol relative" url e.g. "//www.gravatar.com/ava..."
109
- // by resolving the the imagePath relative to the blog url, we can then
110
- // detect if the imagePath is external, or internal.
111
- const isRelativeInternalImage = !isAbsoluteImage && url.resolve(siteUrl, requestedImageUrl).startsWith(siteUrl);
112
-
113
- return isAbsoluteInternalImage || isRelativeInternalImage;
114
- }
115
-
116
- function getUnsplashImage(imagePath, sizeOptions) {
117
- const parsedUrl = new URL(imagePath);
118
- const {requestedSize, imageSizes, requestedFormat} = sizeOptions;
119
-
120
- if (requestedFormat) {
121
- const supportedFormats = ['avif', 'gif', 'jpg', 'png', 'webp'];
122
- if (supportedFormats.includes(requestedFormat)) {
123
- parsedUrl.searchParams.set('fm', requestedFormat);
124
- } else if (requestedFormat === 'jpeg') {
125
- // Map to alias
126
- parsedUrl.searchParams.set('fm', 'jpg');
127
- }
128
- }
129
-
130
- if (!imageSizes || !imageSizes[requestedSize]) {
131
- return parsedUrl.toString();
132
- }
133
-
134
- const {width, height} = imageSizes[requestedSize];
135
-
136
- if (!width && !height) {
137
- return parsedUrl.toString();
138
- }
139
-
140
- parsedUrl.searchParams.delete('w');
141
- parsedUrl.searchParams.delete('h');
142
-
143
- if (width) {
144
- parsedUrl.searchParams.set('w', width);
145
- }
146
- if (height) {
147
- parsedUrl.searchParams.set('h', height);
148
- }
149
- return parsedUrl.toString();
150
- }
151
-
152
- /**
153
- *
154
- * @param {string} imagePath
155
- * @param {Object} sizeOptions
156
- * @param {string} sizeOptions.requestedSize
157
- * @param {Object[]} sizeOptions.imageSizes
158
- * @param {string} [sizeOptions.requestedFormat]
159
- * @returns
160
- */
161
- function getImageWithSize(imagePath, sizeOptions) {
162
- const hasLeadingSlash = imagePath[0] === '/';
163
-
164
- if (hasLeadingSlash) {
165
- return '/' + getImageWithSize(imagePath.slice(1), sizeOptions);
166
- }
167
- const {requestedSize, imageSizes, requestedFormat} = sizeOptions;
168
-
169
- if (!requestedSize) {
170
- return imagePath;
171
- }
172
-
173
- if (!imageSizes || !imageSizes[requestedSize]) {
174
- return imagePath;
175
- }
176
-
177
- const {width, height} = imageSizes[requestedSize];
178
-
179
- if (!width && !height) {
180
- return imagePath;
181
- }
182
-
183
- const [imgBlogUrl, imageName] = imagePath.split(STATIC_IMAGE_URL_PREFIX);
184
-
185
- const sizeDirectoryName = prefixIfPresent('w', width) + prefixIfPresent('h', height);
186
- const formatPrefix = requestedFormat && imageTransform.canTransformToFormat(requestedFormat) ? `/format/${requestedFormat}` : '';
187
-
188
- return [imgBlogUrl, STATIC_IMAGE_URL_PREFIX, `/size/${sizeDirectoryName}`, formatPrefix, imageName].join('');
189
- }
190
-
191
- function prefixIfPresent(prefix, string) {
192
- return string ? prefix + string : '';
193
- }
@@ -1,4 +1,6 @@
1
1
  const _ = require('lodash');
2
+ const {getImageWithSize} = require('../utils/images');
3
+ const config = require('../../shared/config');
2
4
  const imageSizeCache = require('../../server/lib/image').cachedImageSizeFromUrl;
3
5
 
4
6
  /**
@@ -9,23 +11,28 @@ const imageSizeCache = require('../../server/lib/image').cachedImageSizeFromUrl;
9
11
  * called to receive image width and height
10
12
  */
11
13
  async function getImageDimensions(metaData) {
14
+ const MAX_SOCIAL_IMG_WIDTH = config.get('imageOptimization:internalImageSizes:social-image:width') || 1200;
15
+
12
16
  const fetch = {
13
17
  coverImage: imageSizeCache.getCachedImageSizeFromUrl(metaData.coverImage.url),
14
18
  authorImage: imageSizeCache.getCachedImageSizeFromUrl(metaData.authorImage.url),
15
19
  ogImage: imageSizeCache.getCachedImageSizeFromUrl(metaData.ogImage.url),
20
+ twitterImage: imageSizeCache.getCachedImageSizeFromUrl(metaData.twitterImage),
16
21
  logo: imageSizeCache.getCachedImageSizeFromUrl(metaData.site.logo.url)
17
22
  };
18
23
 
19
- const [coverImage, authorImage, ogImage, logo] = await Promise.all([
24
+ const [coverImage, authorImage, ogImage, twitterImage, logo] = await Promise.all([
20
25
  fetch.coverImage,
21
26
  fetch.authorImage,
22
27
  fetch.ogImage,
28
+ fetch.twitterImage,
23
29
  fetch.logo
24
30
  ]);
25
31
  const imageObj = {
26
32
  coverImage,
27
33
  authorImage,
28
34
  ogImage,
35
+ twitterImage,
29
36
  logo
30
37
  };
31
38
 
@@ -53,12 +60,33 @@ async function getImageDimensions(metaData) {
53
60
  });
54
61
  }
55
62
  } else {
56
- _.assign(metaData[value], {
57
- dimensions: {
58
- width: key.width,
59
- height: key.height
63
+ if (key.width > MAX_SOCIAL_IMG_WIDTH) {
64
+ const ratio = key.height / key.width;
65
+ key.width = MAX_SOCIAL_IMG_WIDTH;
66
+ key.height = Math.round(MAX_SOCIAL_IMG_WIDTH * ratio);
67
+
68
+ const sizeOptions = {
69
+ requestedSize: `social-image`,
70
+ imageSizes: config.get('imageOptimization:internalImageSizes')
71
+ };
72
+
73
+ if (typeof metaData[value] === 'string') {
74
+ const url = getImageWithSize(metaData[value], sizeOptions);
75
+ metaData[value] = url;
76
+ } else {
77
+ const url = getImageWithSize(metaData[value].url, sizeOptions);
78
+ _.assign(metaData[value], {url});
60
79
  }
61
- });
80
+ }
81
+
82
+ if (typeof metaData[value] === 'object') {
83
+ _.assign(metaData[value], {
84
+ dimensions: {
85
+ width: key.width,
86
+ height: key.height
87
+ }
88
+ });
89
+ }
62
90
  }
63
91
  }
64
92
  });
@@ -0,0 +1,100 @@
1
+ const url = require('url');
2
+ const imageTransform = require('@tryghost/image-transform');
3
+ const urlUtils = require('../../shared/url-utils');
4
+
5
+ module.exports.detectInternalImage = function detectInternalImage(requestedImageUrl) {
6
+ const siteUrl = urlUtils.getSiteUrl();
7
+ const isAbsoluteImage = /https?:\/\//.test(requestedImageUrl);
8
+ const isAbsoluteInternalImage = isAbsoluteImage && requestedImageUrl.startsWith(siteUrl);
9
+
10
+ // CASE: imagePath is a "protocol relative" url e.g. "//www.gravatar.com/ava..."
11
+ // by resolving the the imagePath relative to the blog url, we can then
12
+ // detect if the imagePath is external, or internal.
13
+ const isRelativeInternalImage = !isAbsoluteImage && url.resolve(siteUrl, requestedImageUrl).startsWith(siteUrl);
14
+
15
+ return isAbsoluteInternalImage || isRelativeInternalImage;
16
+ };
17
+
18
+ module.exports.detectUnsplashImage = function detectUnsplashImage(requestedImageUrl) {
19
+ const isUnsplashImage = /images\.unsplash\.com/.test(requestedImageUrl);
20
+ return isUnsplashImage;
21
+ };
22
+
23
+ module.exports.getUnsplashImage = function getUnsplashImage(imagePath, sizeOptions) {
24
+ const parsedUrl = new URL(imagePath);
25
+ const {requestedSize, imageSizes, requestedFormat} = sizeOptions;
26
+
27
+ if (requestedFormat) {
28
+ const supportedFormats = ['avif', 'gif', 'jpg', 'png', 'webp'];
29
+ if (supportedFormats.includes(requestedFormat)) {
30
+ parsedUrl.searchParams.set('fm', requestedFormat);
31
+ } else if (requestedFormat === 'jpeg') {
32
+ // Map to alias
33
+ parsedUrl.searchParams.set('fm', 'jpg');
34
+ }
35
+ }
36
+
37
+ if (!imageSizes || !imageSizes[requestedSize]) {
38
+ return parsedUrl.toString();
39
+ }
40
+
41
+ const {width, height} = imageSizes[requestedSize];
42
+
43
+ if (!width && !height) {
44
+ return parsedUrl.toString();
45
+ }
46
+
47
+ parsedUrl.searchParams.delete('w');
48
+ parsedUrl.searchParams.delete('h');
49
+
50
+ if (width) {
51
+ parsedUrl.searchParams.set('w', width);
52
+ }
53
+ if (height) {
54
+ parsedUrl.searchParams.set('h', height);
55
+ }
56
+ return parsedUrl.toString();
57
+ };
58
+
59
+ /**
60
+ *
61
+ * @param {string} imagePath
62
+ * @param {Object} sizeOptions
63
+ * @param {string} sizeOptions.requestedSize
64
+ * @param {Object[]} sizeOptions.imageSizes
65
+ * @param {string} [sizeOptions.requestedFormat]
66
+ * @returns
67
+ */
68
+ module.exports.getImageWithSize = function getImageWithSize(imagePath, sizeOptions) {
69
+ const hasLeadingSlash = imagePath[0] === '/';
70
+
71
+ if (hasLeadingSlash) {
72
+ return '/' + getImageWithSize(imagePath.slice(1), sizeOptions);
73
+ }
74
+ const {requestedSize, imageSizes, requestedFormat} = sizeOptions;
75
+
76
+ if (!requestedSize) {
77
+ return imagePath;
78
+ }
79
+
80
+ if (!imageSizes || !imageSizes[requestedSize]) {
81
+ return imagePath;
82
+ }
83
+
84
+ const {width, height} = imageSizes[requestedSize];
85
+
86
+ if (!width && !height) {
87
+ return imagePath;
88
+ }
89
+
90
+ const [imgBlogUrl, imageName] = imagePath.split(urlUtils.STATIC_IMAGE_URL_PREFIX);
91
+
92
+ const sizeDirectoryName = prefixIfPresent('w', width) + prefixIfPresent('h', height);
93
+ const formatPrefix = requestedFormat && imageTransform.canTransformToFormat(requestedFormat) ? `/format/${requestedFormat}` : '';
94
+
95
+ return [imgBlogUrl, urlUtils.STATIC_IMAGE_URL_PREFIX, `/size/${sizeDirectoryName}`, formatPrefix, imageName].join('');
96
+ };
97
+
98
+ function prefixIfPresent(prefix, string) {
99
+ return string ? prefix + string : '';
100
+ }
@@ -40,6 +40,39 @@ async function fetchOrCreatePersonalToken(userId) {
40
40
  return token;
41
41
  }
42
42
 
43
+ function shouldInvalidateCacheAfterChange(model) {
44
+ // Model attributes that should trigger cache invalidation when changed
45
+ // (because they affect the frontend)
46
+ const publicAttrs = [
47
+ 'name',
48
+ 'slug',
49
+ 'profile_image',
50
+ 'cover_image',
51
+ 'bio',
52
+ 'website',
53
+ 'location',
54
+ 'facebook',
55
+ 'twitter',
56
+ 'status',
57
+ 'visibility',
58
+ 'meta_title',
59
+ 'meta_description'
60
+ ];
61
+
62
+ if (model.wasChanged() === false) {
63
+ return false;
64
+ }
65
+
66
+ // Check if any of the changed attributes are public
67
+ for (const attr of Object.keys(model._changed)) {
68
+ if (publicAttrs.includes(attr) === true) {
69
+ return true;
70
+ }
71
+ }
72
+
73
+ return false;
74
+ }
75
+
43
76
  module.exports = {
44
77
  docName: 'users',
45
78
 
@@ -137,11 +170,7 @@ module.exports = {
137
170
  }));
138
171
  }
139
172
 
140
- if (model.wasChanged()) {
141
- this.headers.cacheInvalidate = true;
142
- } else {
143
- this.headers.cacheInvalidate = false;
144
- }
173
+ this.headers.cacheInvalidate = shouldInvalidateCacheAfterChange(model);
145
174
 
146
175
  return model;
147
176
  });
@@ -17,7 +17,7 @@ module.exports = {
17
17
  slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
18
18
  sender_name: {type: 'string', maxlength: 191, nullable: true},
19
19
  sender_email: {type: 'string', maxlength: 191, nullable: true},
20
- sender_reply_to: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'newsletter', validations: {isIn: [['newsletter', 'support']]}},
20
+ sender_reply_to: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'newsletter'},
21
21
  status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'active', validations: {isIn: [['active', 'archived']]}},
22
22
  visibility: {
23
23
  type: 'string',
@@ -7,6 +7,10 @@ const schema = require('../../../data/schema');
7
7
  // This wires up our model event system
8
8
  const events = require('../../../lib/common/events');
9
9
 
10
+ // Run tests or development with NUMERIC_IDS=1 to enable numeric object IDs
11
+ let forceNumericObjectIds = process.env.NODE_ENV !== 'production' && !!process.env.NUMERIC_IDS;
12
+ let numberGenerator = 0;
13
+
10
14
  module.exports = function (Bookshelf) {
11
15
  Bookshelf.Model = Bookshelf.Model.extend({
12
16
  initializeEvents: function () {
@@ -39,7 +43,7 @@ module.exports = function (Bookshelf) {
39
43
  * no auto increment
40
44
  */
41
45
  setId: function setId() {
42
- this.set('id', ObjectId().toHexString());
46
+ this.set('id', Bookshelf.Model.generateId());
43
47
  },
44
48
 
45
49
  /**
@@ -268,5 +272,20 @@ module.exports = function (Bookshelf) {
268
272
 
269
273
  this.addAction(model, 'deleted', options);
270
274
  }
275
+ }, {
276
+ generateId: function generateId() {
277
+ if (forceNumericObjectIds) {
278
+ numberGenerator = numberGenerator + 1;
279
+ const counter = numberGenerator.toString();
280
+
281
+ // 77777777 here are to make sure generated ids's are larger than naturally generated ones
282
+ const base = '777777770000000000000000';
283
+ const id = base.substring(0, base.length - counter.length) + counter;
284
+
285
+ //// This always generates a valid object ID that is fully numeric
286
+ return id;
287
+ }
288
+ return ObjectId().toHexString();
289
+ }
271
290
  });
272
291
  };
@@ -0,0 +1,39 @@
1
+ class EmailAddressServiceWrapper {
2
+ /**
3
+ * @type {import('@tryghost/email-addresses').EmailAddressService}
4
+ */
5
+ service;
6
+
7
+ init() {
8
+ if (this.service) {
9
+ return;
10
+ }
11
+
12
+ const labs = require('../../../shared/labs');
13
+ const config = require('../../../shared/config');
14
+ const settingsHelpers = require('../settings-helpers');
15
+ const validator = require('@tryghost/validator');
16
+
17
+ const {
18
+ EmailAddressService
19
+ } = require('@tryghost/email-addresses');
20
+
21
+ this.service = new EmailAddressService({
22
+ labs,
23
+ getManagedEmailEnabled: () => {
24
+ return config.get('hostSettings:managedEmail:enabled') ?? false;
25
+ },
26
+ getSendingDomain: () => {
27
+ return config.get('hostSettings:managedEmail:sendingDomain') || null;
28
+ },
29
+ getDefaultEmail: () => {
30
+ return settingsHelpers.getDefaultEmail();
31
+ },
32
+ isValidEmailAddress: (emailAddress) => {
33
+ return validator.isEmail(emailAddress);
34
+ }
35
+ });
36
+ }
37
+ }
38
+
39
+ module.exports = EmailAddressServiceWrapper;
@@ -0,0 +1,3 @@
1
+ const EmailAddressServiceWrapper = require('./EmailAddressServiceWrapper');
2
+
3
+ module.exports = new EmailAddressServiceWrapper();
@@ -26,6 +26,7 @@ class EmailServiceWrapper {
26
26
  const membersRepository = membersService.api.members;
27
27
  const limitService = require('../limits');
28
28
  const labs = require('../../../shared/labs');
29
+ const emailAddressService = require('../email-address');
29
30
 
30
31
  const mobiledocLib = require('../../lib/mobiledoc');
31
32
  const lexicalLib = require('../../lib/lexical');
@@ -70,6 +71,7 @@ class EmailServiceWrapper {
70
71
  memberAttributionService: memberAttribution.service,
71
72
  audienceFeedbackService: audienceFeedback.service,
72
73
  outboundLinkTagger: memberAttribution.outboundLinkTagger,
74
+ emailAddressService: emailAddressService.service,
73
75
  labs,
74
76
  models: {Post}
75
77
  });
@@ -8,6 +8,8 @@ const tpl = require('@tryghost/tpl');
8
8
  const settingsCache = require('../../../shared/settings-cache');
9
9
  const urlUtils = require('../../../shared/url-utils');
10
10
  const metrics = require('@tryghost/metrics');
11
+ const settingsHelpers = require('../settings-helpers');
12
+ const emailAddress = require('../email-address');
11
13
  const messages = {
12
14
  title: 'Ghost at {domain}',
13
15
  checkEmailConfigInstructions: 'Please see {url} for instructions on configuring email.',
@@ -16,29 +18,59 @@ const messages = {
16
18
  reason: ' Reason: {reason}.',
17
19
  messageSent: 'Message sent. Double check inbox and spam folder!'
18
20
  };
21
+ const {EmailAddressParser} = require('@tryghost/email-addresses');
22
+ const logging = require('@tryghost/logging');
19
23
 
20
24
  function getDomain() {
21
25
  const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
22
26
  return domain && domain[1];
23
27
  }
24
28
 
25
- function getFromAddress(requestedFromAddress) {
29
+ /**
30
+ * @param {string} requestedFromAddress
31
+ * @param {string} requestedReplyToAddress
32
+ * @returns {{from: string, replyTo?: string|null}}
33
+ */
34
+ function getFromAddress(requestedFromAddress, requestedReplyToAddress) {
35
+ if (settingsHelpers.useNewEmailAddresses()) {
36
+ if (!requestedFromAddress) {
37
+ // Use the default config
38
+ requestedFromAddress = emailAddress.service.defaultFromEmail;
39
+ }
40
+
41
+ // Clean up email addresses (checks whether sending is allowed + email address is valid)
42
+ const addresses = emailAddress.service.getAddressFromString(requestedFromAddress, requestedReplyToAddress);
43
+
44
+ // fill in missing name if not set
45
+ const defaultSiteTitle = settingsCache.get('title') ? settingsCache.get('title') : tpl(messages.title, {domain: getDomain()});
46
+ if (!addresses.from.name) {
47
+ addresses.from.name = defaultSiteTitle;
48
+ }
49
+
50
+ return {
51
+ from: EmailAddressParser.stringify(addresses.from),
52
+ replyTo: addresses.replyTo ? EmailAddressParser.stringify(addresses.replyTo) : null
53
+ };
54
+ }
26
55
  const configAddress = config.get('mail') && config.get('mail').from;
27
56
 
28
57
  const address = requestedFromAddress || configAddress;
29
58
  // If we don't have a from address at all
30
59
  if (!address) {
31
60
  // Default to noreply@[blog.url]
32
- return getFromAddress(`noreply@${getDomain()}`);
61
+ return getFromAddress(`noreply@${getDomain()}`, requestedReplyToAddress);
33
62
  }
34
63
 
35
64
  // If we do have a from address, and it's just an email
36
65
  if (validator.isEmail(address, {require_tld: false})) {
37
66
  const defaultSiteTitle = settingsCache.get('title') ? settingsCache.get('title').replace(/"/g, '\\"') : tpl(messages.title, {domain: getDomain()});
38
- return `"${defaultSiteTitle}" <${address}>`;
67
+ return {
68
+ from: `"${defaultSiteTitle}" <${address}>`
69
+ };
39
70
  }
40
71
 
41
- return address;
72
+ logging.warn(`Invalid from address used for sending emails: ${address}`);
73
+ return {from: address};
42
74
  }
43
75
 
44
76
  /**
@@ -47,16 +79,21 @@ function getFromAddress(requestedFromAddress) {
47
79
  * @param {Object} message
48
80
  * @param {boolean} [message.forceTextContent] - force text content
49
81
  * @param {string} [message.from] - sender email address
82
+ * @param {string} [message.replyTo]
50
83
  * @returns {Object}
51
84
  */
52
85
  function createMessage(message) {
53
86
  const encoding = 'base64';
54
87
  const generateTextFromHTML = !message.forceTextContent;
55
- return Object.assign({}, message, {
56
- from: getFromAddress(message.from),
88
+
89
+ const addresses = getFromAddress(message.from, message.replyTo);
90
+
91
+ return {
92
+ ...message,
93
+ ...addresses,
57
94
  generateTextFromHTML,
58
95
  encoding
59
- });
96
+ };
60
97
  }
61
98
 
62
99
  function createMailError({message, err, ignoreDefaultMessage} = {message: ''}) {
@@ -154,13 +191,13 @@ module.exports = class GhostMailer {
154
191
  return tpl(messages.messageSent);
155
192
  }
156
193
 
157
- if (response.pending.length > 0) {
194
+ if (response.pending && response.pending.length > 0) {
158
195
  throw createMailError({
159
196
  message: tpl(messages.reason, {reason: 'Email has been temporarily rejected'})
160
197
  });
161
198
  }
162
199
 
163
- if (response.errors.length > 0) {
200
+ if (response.errors && response.errors.length > 0) {
164
201
  throw createMailError({
165
202
  message: tpl(messages.reason, {reason: response.errors[0].message})
166
203
  });
@@ -89,7 +89,13 @@ const initVerificationTrigger = () => {
89
89
  isVerificationRequired: () => settingsCache.get('email_verification_required') === true,
90
90
  sendVerificationEmail: async ({subject, message, amountTriggered}) => {
91
91
  const escalationAddress = config.get('hostSettings:emailVerification:escalationAddress');
92
- const fromAddress = config.get('user_email');
92
+ let fromAddress = config.get('user_email');
93
+ let replyTo = undefined;
94
+
95
+ if (settingsHelpers.useNewEmailAddresses()) {
96
+ replyTo = fromAddress;
97
+ fromAddress = settingsHelpers.getNoReplyAddress();
98
+ }
93
99
 
94
100
  if (escalationAddress) {
95
101
  await ghostMailer.send({
@@ -100,6 +106,7 @@ const initVerificationTrigger = () => {
100
106
  }),
101
107
  forceTextContent: true,
102
108
  from: fromAddress,
109
+ replyTo,
103
110
  to: escalationAddress
104
111
  });
105
112
  }