ghost 5.97.3 → 5.98.1

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 (196) hide show
  1. package/components/tryghost-adapter-cache-memory-ttl-5.98.1.tgz +0 -0
  2. package/components/tryghost-adapter-cache-redis-5.98.1.tgz +0 -0
  3. package/components/tryghost-adapter-manager-5.98.1.tgz +0 -0
  4. package/components/tryghost-announcement-bar-settings-5.98.1.tgz +0 -0
  5. package/components/{tryghost-api-framework-5.97.3.tgz → tryghost-api-framework-5.98.1.tgz} +0 -0
  6. package/components/tryghost-api-version-compatibility-service-5.98.1.tgz +0 -0
  7. package/components/tryghost-audience-feedback-5.98.1.tgz +0 -0
  8. package/components/tryghost-bookshelf-repository-5.98.1.tgz +0 -0
  9. package/components/{tryghost-bootstrap-socket-5.97.3.tgz → tryghost-bootstrap-socket-5.98.1.tgz} +0 -0
  10. package/components/tryghost-collections-5.98.1.tgz +0 -0
  11. package/components/tryghost-constants-5.98.1.tgz +0 -0
  12. package/components/tryghost-custom-fonts-5.98.1.tgz +0 -0
  13. package/components/tryghost-custom-theme-settings-service-5.98.1.tgz +0 -0
  14. package/components/{tryghost-data-generator-5.97.3.tgz → tryghost-data-generator-5.98.1.tgz} +0 -0
  15. package/components/tryghost-domain-events-5.98.1.tgz +0 -0
  16. package/components/tryghost-donations-5.98.1.tgz +0 -0
  17. package/components/tryghost-dynamic-routing-events-5.98.1.tgz +0 -0
  18. package/components/tryghost-email-addresses-5.98.1.tgz +0 -0
  19. package/components/tryghost-email-analytics-provider-mailgun-5.98.1.tgz +0 -0
  20. package/components/tryghost-email-analytics-service-5.98.1.tgz +0 -0
  21. package/components/{tryghost-email-content-generator-5.97.3.tgz → tryghost-email-content-generator-5.98.1.tgz} +0 -0
  22. package/components/tryghost-email-events-5.98.1.tgz +0 -0
  23. package/components/{tryghost-email-service-5.97.3.tgz → tryghost-email-service-5.98.1.tgz} +0 -0
  24. package/components/tryghost-email-suppression-list-5.98.1.tgz +0 -0
  25. package/components/tryghost-express-dynamic-redirects-5.98.1.tgz +0 -0
  26. package/components/tryghost-external-media-inliner-5.98.1.tgz +0 -0
  27. package/components/tryghost-extract-api-key-5.98.1.tgz +0 -0
  28. package/components/tryghost-ghost-5.98.1.tgz +0 -0
  29. package/components/tryghost-html-to-plaintext-5.98.1.tgz +0 -0
  30. package/components/tryghost-i18n-5.98.1.tgz +0 -0
  31. package/components/tryghost-importer-handler-content-files-5.98.1.tgz +0 -0
  32. package/components/{tryghost-importer-revue-5.97.3.tgz → tryghost-importer-revue-5.98.1.tgz} +0 -0
  33. package/components/tryghost-in-memory-repository-5.98.1.tgz +0 -0
  34. package/components/{tryghost-job-manager-5.97.3.tgz → tryghost-job-manager-5.98.1.tgz} +0 -0
  35. package/components/tryghost-link-redirects-5.98.1.tgz +0 -0
  36. package/components/tryghost-link-replacer-5.98.1.tgz +0 -0
  37. package/components/{tryghost-link-tracking-5.97.3.tgz → tryghost-link-tracking-5.98.1.tgz} +0 -0
  38. package/components/tryghost-magic-link-5.98.1.tgz +0 -0
  39. package/components/{tryghost-mail-events-5.97.3.tgz → tryghost-mail-events-5.98.1.tgz} +0 -0
  40. package/components/tryghost-mailgun-client-5.98.1.tgz +0 -0
  41. package/components/tryghost-member-attribution-5.98.1.tgz +0 -0
  42. package/components/tryghost-member-events-5.98.1.tgz +0 -0
  43. package/components/tryghost-members-api-5.98.1.tgz +0 -0
  44. package/components/tryghost-members-csv-5.98.1.tgz +0 -0
  45. package/components/tryghost-members-events-service-5.98.1.tgz +0 -0
  46. package/components/{tryghost-members-importer-5.97.3.tgz → tryghost-members-importer-5.98.1.tgz} +0 -0
  47. package/components/{tryghost-members-offers-5.97.3.tgz → tryghost-members-offers-5.98.1.tgz} +0 -0
  48. package/components/tryghost-members-payments-5.98.1.tgz +0 -0
  49. package/components/tryghost-members-ssr-5.98.1.tgz +0 -0
  50. package/components/tryghost-members-stripe-service-5.98.1.tgz +0 -0
  51. package/components/tryghost-mentions-email-report-5.98.1.tgz +0 -0
  52. package/components/tryghost-metrics-server-5.98.1.tgz +0 -0
  53. package/components/tryghost-milestones-5.98.1.tgz +0 -0
  54. package/components/tryghost-minifier-5.98.1.tgz +0 -0
  55. package/components/tryghost-model-to-domain-event-interceptor-5.98.1.tgz +0 -0
  56. package/components/tryghost-mw-api-version-mismatch-5.98.1.tgz +0 -0
  57. package/components/tryghost-mw-cache-control-5.98.1.tgz +0 -0
  58. package/components/tryghost-mw-error-handler-5.98.1.tgz +0 -0
  59. package/components/tryghost-mw-session-from-token-5.98.1.tgz +0 -0
  60. package/components/tryghost-mw-update-user-last-seen-5.98.1.tgz +0 -0
  61. package/components/tryghost-mw-version-match-5.98.1.tgz +0 -0
  62. package/components/tryghost-mw-vhost-5.98.1.tgz +0 -0
  63. package/components/tryghost-nql-filter-expansions-5.98.1.tgz +0 -0
  64. package/components/{tryghost-oembed-service-5.97.3.tgz → tryghost-oembed-service-5.98.1.tgz} +0 -0
  65. package/components/tryghost-package-json-5.98.1.tgz +0 -0
  66. package/components/tryghost-post-events-5.98.1.tgz +0 -0
  67. package/components/tryghost-post-revisions-5.98.1.tgz +0 -0
  68. package/components/tryghost-posts-service-5.98.1.tgz +0 -0
  69. package/components/tryghost-recommendations-5.98.1.tgz +0 -0
  70. package/components/tryghost-referrers-5.98.1.tgz +0 -0
  71. package/components/tryghost-security-5.98.1.tgz +0 -0
  72. package/components/tryghost-session-service-5.98.1.tgz +0 -0
  73. package/components/tryghost-settings-path-manager-5.98.1.tgz +0 -0
  74. package/components/tryghost-slack-notifications-5.98.1.tgz +0 -0
  75. package/components/{tryghost-staff-service-5.97.3.tgz → tryghost-staff-service-5.98.1.tgz} +0 -0
  76. package/components/{tryghost-stats-service-5.97.3.tgz → tryghost-stats-service-5.98.1.tgz} +0 -0
  77. package/components/{tryghost-tiers-5.97.3.tgz → tryghost-tiers-5.98.1.tgz} +0 -0
  78. package/components/tryghost-update-check-service-5.98.1.tgz +0 -0
  79. package/components/tryghost-verification-trigger-5.98.1.tgz +0 -0
  80. package/components/tryghost-version-notifications-data-service-5.98.1.tgz +0 -0
  81. package/components/tryghost-webmentions-5.98.1.tgz +0 -0
  82. package/content/themes/source/assets/built/screen.css +1 -1
  83. package/content/themes/source/assets/built/screen.css.map +1 -1
  84. package/content/themes/source/assets/css/screen.css +3 -2
  85. package/content/themes/source/package.json +2 -2
  86. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +3 -3
  87. package/core/built/admin/assets/admin-x-activitypub/{index-ce1044b4.mjs → index-d0087e93.mjs} +4049 -3812
  88. package/core/built/admin/assets/admin-x-activitypub/modals-aaaeb2ed.mjs +218 -0
  89. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +2 -2
  90. package/core/built/admin/assets/admin-x-demo/{index-4e95bd22.mjs → index-ec1c4705.mjs} +1516 -1510
  91. package/core/built/admin/assets/admin-x-demo/{modals-1dd517c9.mjs → modals-3bfb50e8.mjs} +2 -2
  92. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-56b3751e.mjs → CodeEditorView-72579105.mjs} +2 -2
  93. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +3 -3
  94. package/core/built/admin/assets/admin-x-settings/{index-a57b83d6.mjs → index-253cc6ec.mjs} +6929 -6721
  95. package/core/built/admin/assets/admin-x-settings/{index-ae6dfa81.mjs → index-c8b38805.mjs} +2 -2
  96. package/core/built/admin/assets/admin-x-settings/{modals-8dbfe675.mjs → modals-e1b17636.mjs} +7277 -7295
  97. package/core/built/admin/assets/{chunk.524.b095de8463657e362a7a.js → chunk.524.61222a4a218eee1d5708.js} +5 -5
  98. package/core/built/admin/assets/{chunk.582.32fd06c8434d4db3f0a8.js → chunk.582.2b6d8d14e037cb327afb.js} +6 -6
  99. package/core/built/admin/assets/{ghost-b7702081322ac2a966ad205b1d07416c.js → ghost-7a6db87f125f7ea41beb7a88d500e8dc.js} +173 -144
  100. package/core/built/admin/assets/ghost-dark-4781c0dc5c36ec188a1ba04d893dbe47.css +1 -0
  101. package/core/built/admin/index.html +3 -3
  102. package/core/frontend/apps/amp/lib/views/amp.hbs +1 -1
  103. package/core/frontend/helpers/body_class.js +32 -0
  104. package/core/frontend/helpers/content_api_url.js +28 -0
  105. package/core/frontend/helpers/ghost_head.js +32 -3
  106. package/core/frontend/meta/schema.js +1 -0
  107. package/core/frontend/services/theme-engine/preview.js +3 -1
  108. package/core/server/api/endpoints/session.js +12 -2
  109. package/core/server/data/migrations/versions/5.97/2024-10-09-14-04-10-add-session-verification-field.js +25 -0
  110. package/core/server/models/comment.js +25 -0
  111. package/core/server/services/auth/session/SessionStore.js +0 -6
  112. package/core/server/services/auth/session/index.js +22 -1
  113. package/core/server/services/auth/session/middleware.js +55 -6
  114. package/core/server/services/url/Queue.js +14 -13
  115. package/core/server/services/url/Resources.js +4 -3
  116. package/core/server/services/url/UrlGenerator.js +4 -8
  117. package/core/server/services/url/UrlService.js +2 -3
  118. package/core/server/services/url/Urls.js +3 -6
  119. package/core/server/web/api/endpoints/admin/routes.js +2 -0
  120. package/core/server/web/shared/middleware/api/spam-prevention.js +62 -0
  121. package/core/server/web/shared/middleware/brute.js +22 -0
  122. package/core/shared/config/defaults.json +15 -3
  123. package/core/shared/config/env/config.testing-browser.json +12 -0
  124. package/core/shared/config/env/config.testing-mysql.json +12 -0
  125. package/core/shared/config/env/config.testing.json +12 -0
  126. package/core/shared/labs.js +3 -1
  127. package/nodemon.json +3 -0
  128. package/package.json +160 -154
  129. package/yarn.lock +191 -103
  130. package/components/tryghost-adapter-cache-memory-ttl-5.97.3.tgz +0 -0
  131. package/components/tryghost-adapter-cache-redis-5.97.3.tgz +0 -0
  132. package/components/tryghost-adapter-manager-5.97.3.tgz +0 -0
  133. package/components/tryghost-announcement-bar-settings-5.97.3.tgz +0 -0
  134. package/components/tryghost-api-version-compatibility-service-5.97.3.tgz +0 -0
  135. package/components/tryghost-audience-feedback-5.97.3.tgz +0 -0
  136. package/components/tryghost-bookshelf-repository-5.97.3.tgz +0 -0
  137. package/components/tryghost-collections-5.97.3.tgz +0 -0
  138. package/components/tryghost-constants-5.97.3.tgz +0 -0
  139. package/components/tryghost-custom-theme-settings-service-5.97.3.tgz +0 -0
  140. package/components/tryghost-domain-events-5.97.3.tgz +0 -0
  141. package/components/tryghost-donations-5.97.3.tgz +0 -0
  142. package/components/tryghost-dynamic-routing-events-5.97.3.tgz +0 -0
  143. package/components/tryghost-email-addresses-5.97.3.tgz +0 -0
  144. package/components/tryghost-email-analytics-provider-mailgun-5.97.3.tgz +0 -0
  145. package/components/tryghost-email-analytics-service-5.97.3.tgz +0 -0
  146. package/components/tryghost-email-events-5.97.3.tgz +0 -0
  147. package/components/tryghost-email-suppression-list-5.97.3.tgz +0 -0
  148. package/components/tryghost-express-dynamic-redirects-5.97.3.tgz +0 -0
  149. package/components/tryghost-external-media-inliner-5.97.3.tgz +0 -0
  150. package/components/tryghost-extract-api-key-5.97.3.tgz +0 -0
  151. package/components/tryghost-ghost-5.97.3.tgz +0 -0
  152. package/components/tryghost-html-to-plaintext-5.97.3.tgz +0 -0
  153. package/components/tryghost-i18n-5.97.3.tgz +0 -0
  154. package/components/tryghost-importer-handler-content-files-5.97.3.tgz +0 -0
  155. package/components/tryghost-in-memory-repository-5.97.3.tgz +0 -0
  156. package/components/tryghost-link-redirects-5.97.3.tgz +0 -0
  157. package/components/tryghost-link-replacer-5.97.3.tgz +0 -0
  158. package/components/tryghost-magic-link-5.97.3.tgz +0 -0
  159. package/components/tryghost-mailgun-client-5.97.3.tgz +0 -0
  160. package/components/tryghost-member-attribution-5.97.3.tgz +0 -0
  161. package/components/tryghost-member-events-5.97.3.tgz +0 -0
  162. package/components/tryghost-members-api-5.97.3.tgz +0 -0
  163. package/components/tryghost-members-csv-5.97.3.tgz +0 -0
  164. package/components/tryghost-members-events-service-5.97.3.tgz +0 -0
  165. package/components/tryghost-members-payments-5.97.3.tgz +0 -0
  166. package/components/tryghost-members-ssr-5.97.3.tgz +0 -0
  167. package/components/tryghost-members-stripe-service-5.97.3.tgz +0 -0
  168. package/components/tryghost-mentions-email-report-5.97.3.tgz +0 -0
  169. package/components/tryghost-metrics-server-5.97.3.tgz +0 -0
  170. package/components/tryghost-milestones-5.97.3.tgz +0 -0
  171. package/components/tryghost-minifier-5.97.3.tgz +0 -0
  172. package/components/tryghost-model-to-domain-event-interceptor-5.97.3.tgz +0 -0
  173. package/components/tryghost-mw-api-version-mismatch-5.97.3.tgz +0 -0
  174. package/components/tryghost-mw-cache-control-5.97.3.tgz +0 -0
  175. package/components/tryghost-mw-error-handler-5.97.3.tgz +0 -0
  176. package/components/tryghost-mw-session-from-token-5.97.3.tgz +0 -0
  177. package/components/tryghost-mw-update-user-last-seen-5.97.3.tgz +0 -0
  178. package/components/tryghost-mw-version-match-5.97.3.tgz +0 -0
  179. package/components/tryghost-mw-vhost-5.97.3.tgz +0 -0
  180. package/components/tryghost-nql-filter-expansions-5.97.3.tgz +0 -0
  181. package/components/tryghost-package-json-5.97.3.tgz +0 -0
  182. package/components/tryghost-post-events-5.97.3.tgz +0 -0
  183. package/components/tryghost-post-revisions-5.97.3.tgz +0 -0
  184. package/components/tryghost-posts-service-5.97.3.tgz +0 -0
  185. package/components/tryghost-recommendations-5.97.3.tgz +0 -0
  186. package/components/tryghost-referrers-5.97.3.tgz +0 -0
  187. package/components/tryghost-security-5.97.3.tgz +0 -0
  188. package/components/tryghost-session-service-5.97.3.tgz +0 -0
  189. package/components/tryghost-settings-path-manager-5.97.3.tgz +0 -0
  190. package/components/tryghost-slack-notifications-5.97.3.tgz +0 -0
  191. package/components/tryghost-update-check-service-5.97.3.tgz +0 -0
  192. package/components/tryghost-verification-trigger-5.97.3.tgz +0 -0
  193. package/components/tryghost-version-notifications-data-service-5.97.3.tgz +0 -0
  194. package/components/tryghost-webmentions-5.97.3.tgz +0 -0
  195. package/core/built/admin/assets/admin-x-activitypub/modals-35dd1224.mjs +0 -250
  196. package/core/built/admin/assets/ghost-dark-b00268cf596f7bba8699dce557d65585.css +0 -1
@@ -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.97%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%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%22ee37f0b9eb%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%2224d6ef8fbd%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%22b1e0cd5a92%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%229368ade858%22%2C%22adminXActivitypubCustomUrl%22%3A%22https%3A%2F%2Fcdn.jsdelivr.net%2Fghost%2Fadmin-x-activitypub%400%2Fdist%2Fadmin-x-activitypub.js%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.98%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%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%22ee37f0b9eb%22%2C%22adminXDemoFilename%22%3A%22admin-x-demo.js%22%2C%22adminXDemoHash%22%3A%221b17317b1f%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%22a887193dcd%22%2C%22adminXActivitypubFilename%22%3A%22admin-x-activitypub.js%22%2C%22adminXActivitypubHash%22%3A%22ad0a25d390%22%2C%22adminXActivitypubCustomUrl%22%3A%22https%3A%2F%2Fcdn.jsdelivr.net%2Fghost%2Fadmin-x-activitypub%400%2Fdist%2Fadmin-x-activitypub.js%22%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -58,7 +58,7 @@
58
58
 
59
59
  <script src="assets/vendor-88cd9f5cf5eba65221e2d2a636531eaa.js"></script>
60
60
  <script src="assets/chunk.874.5eb920d19e75683234c2.js"></script>
61
- <script src="assets/chunk.524.b095de8463657e362a7a.js"></script>
62
- <script src="assets/ghost-b7702081322ac2a966ad205b1d07416c.js"></script>
61
+ <script src="assets/chunk.524.61222a4a218eee1d5708.js"></script>
62
+ <script src="assets/ghost-7a6db87f125f7ea41beb7a88d500e8dc.js"></script>
63
63
  </body>
64
64
  </html>
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html ⚡>
2
+ <html ⚡ lang="{{@site.locale}}">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
@@ -2,8 +2,14 @@
2
2
  // Usage: `{{body_class}}`
3
3
  //
4
4
  // Output classes for the body element
5
+ const {labs, settingsCache} = require('../services/proxy');
6
+ const {generateCustomFontBodyClass, isValidCustomFont, isValidCustomHeadingFont} = require('@tryghost/custom-fonts');
5
7
  const {SafeString} = require('../services/handlebars');
6
8
 
9
+ /**
10
+ * @typedef {import('@tryghost/custom-fonts').FontSelection} FontSelection
11
+ */
12
+
7
13
  // We use the name body_class to match the helper for consistency
8
14
  module.exports = function body_class(options) { // eslint-disable-line camelcase
9
15
  let classes = [];
@@ -39,6 +45,32 @@ module.exports = function body_class(options) { // eslint-disable-line camelcase
39
45
  classes.push('paged');
40
46
  }
41
47
 
48
+ if (labs.isSet('customFonts')) {
49
+ // Check if if the request is for a site preview, in which case we **always** use the custom font values
50
+ // from the passed in data, even when they're empty strings or settings cache has values.
51
+ const isSitePreview = options.data.site._preview;
52
+ // Taking the fonts straight from the passed in data, as they can't be used from the
53
+ // settings cache for the theme preview until the settings are saved. Once saved,
54
+ // we need to use the settings cache to provide the correct CSS injection.
55
+ const headingFont = isSitePreview ? options.data.site.heading_font : settingsCache.get('heading_font');
56
+ const bodyFont = isSitePreview ? options.data.site.body_font : settingsCache.get('body_font');
57
+
58
+ if ((typeof headingFont === 'string' && isValidCustomHeadingFont(headingFont)) ||
59
+ (typeof bodyFont === 'string' && isValidCustomFont(bodyFont))) {
60
+ /** @type FontSelection */
61
+ const fontSelection = {};
62
+
63
+ if (headingFont) {
64
+ fontSelection.heading = headingFont;
65
+ }
66
+ if (bodyFont) {
67
+ fontSelection.body = bodyFont;
68
+ }
69
+ const customBodyClasses = generateCustomFontBodyClass(fontSelection);
70
+ classes.push(new SafeString(customBodyClasses));
71
+ }
72
+ }
73
+
42
74
  classes = classes.join(' ').trim();
43
75
 
44
76
  return new SafeString(classes);
@@ -0,0 +1,28 @@
1
+ const {SafeString} = require('../services/handlebars');
2
+ const logging = require('@tryghost/logging');
3
+ const {urlUtils} = require('../services/proxy');
4
+
5
+ module.exports = function content_api_url(options) { // eslint-disable-line camelcase
6
+ let result;
7
+ const absoluteUrlRequested = getAbsoluteOption(options);
8
+
9
+ try {
10
+ let path = urlUtils.urlFor('api', {type: 'content'}, absoluteUrlRequested);
11
+ result = new SafeString(path);
12
+ } catch (error) {
13
+ logging.error(error);
14
+ result = '';
15
+ }
16
+
17
+ return result;
18
+ };
19
+
20
+ function getAbsoluteOption(options) {
21
+ const absoluteOption = options && options.hash && options.hash.absolute;
22
+ if (absoluteOption === undefined || absoluteOption === 'true' || absoluteOption === true || absoluteOption === null) {
23
+ return true;
24
+ } else {
25
+ return false;
26
+ }
27
+ }
28
+
@@ -4,7 +4,7 @@
4
4
  // Outputs scripts and other assets at the top of a Ghost theme
5
5
  const {labs, metaData, settingsCache, config, blogIcon, urlUtils, getFrontendKey} = require('../services/proxy');
6
6
  const {escapeExpression, SafeString} = require('../services/handlebars');
7
-
7
+ const {generateCustomFontCss, isValidCustomFont, isValidCustomHeadingFont} = require('@tryghost/custom-fonts');
8
8
  // BAD REQUIRE
9
9
  // @TODO fix this require
10
10
  const {cardAssets} = require('../services/assets-minification');
@@ -15,6 +15,10 @@ const debug = require('@tryghost/debug')('ghost_head');
15
15
  const templateStyles = require('./tpl/styles');
16
16
  const {getFrontendAppConfig, getDataAttributes} = require('../utils/frontend-apps');
17
17
 
18
+ /**
19
+ * @typedef {import('@tryghost/custom-fonts').FontSelection} FontSelection
20
+ */
21
+
18
22
  const {get: getMetaData, getAssetUrl} = metaData;
19
23
 
20
24
  function writeMetaTag(property, content, type) {
@@ -45,8 +49,8 @@ function finaliseStructuredData(meta) {
45
49
  }
46
50
 
47
51
  function getMembersHelper(data, frontendKey) {
48
- // Do not load Portal if both Memberships and Tips & Donations are disabled
49
- if (!settingsCache.get('members_enabled') && !settingsCache.get('donations_enabled')) {
52
+ // Do not load Portal if both Memberships and Tips & Donations and Recommendations are disabled
53
+ if (!settingsCache.get('members_enabled') && !settingsCache.get('donations_enabled') && !settingsCache.get('recommendations_enabled')) {
50
54
  return '';
51
55
  }
52
56
 
@@ -339,6 +343,31 @@ module.exports = async function ghost_head(options) { // eslint-disable-line cam
339
343
  if (config.get('tinybird') && config.get('tinybird:tracker') && config.get('tinybird:tracker:scriptUrl')) {
340
344
  head.push(getTinybirdTrackerScript(dataRoot));
341
345
  }
346
+
347
+ if (labs.isSet('customFonts')) {
348
+ // Check if if the request is for a site preview, in which case we **always** use the custom font values
349
+ // from the passed in data, even when they're empty strings or settings cache has values.
350
+ const isSitePreview = options.data.site._preview;
351
+ // Taking the fonts straight from the passed in data, as they can't be used from the
352
+ // settings cache for the theme preview until the settings are saved. Once saved,
353
+ // we need to use the settings cache to provide the correct CSS injection.
354
+ const headingFont = isSitePreview ? options.data.site.heading_font : settingsCache.get('heading_font');
355
+ const bodyFont = isSitePreview ? options.data.site.body_font : settingsCache.get('body_font');
356
+ if ((typeof headingFont === 'string' && isValidCustomHeadingFont(headingFont)) ||
357
+ (typeof bodyFont === 'string' && isValidCustomFont(bodyFont))) {
358
+ /** @type FontSelection */
359
+ const fontSelection = {};
360
+
361
+ if (headingFont) {
362
+ fontSelection.heading = headingFont;
363
+ }
364
+ if (bodyFont) {
365
+ fontSelection.body = bodyFont;
366
+ }
367
+ const customCSS = generateCustomFontCss(fontSelection);
368
+ head.push(new SafeString(customCSS));
369
+ }
370
+ }
342
371
  }
343
372
 
344
373
  debug('end');
@@ -119,6 +119,7 @@ function getHomeSchema(metaData) {
119
119
  '@type': 'WebSite',
120
120
  publisher: schemaPublisherObject(metaData),
121
121
  url: metaData.url,
122
+ name: metaData.site.title,
122
123
  image: schemaImageObject(metaData.coverImage),
123
124
  mainEntityOfPage: metaData.url,
124
125
  description: metaData.metaDescription ?
@@ -22,7 +22,9 @@ function getPreviewData(previewHeader, customThemeSettingKeys = []) {
22
22
  logo: 'logo',
23
23
  cover: 'cover_image',
24
24
  custom: 'custom',
25
- d: 'description'
25
+ d: 'description',
26
+ bf: 'body_font',
27
+ hf: 'heading_font'
26
28
  };
27
29
 
28
30
  let opts = new URLSearchParams(previewHeader);
@@ -61,8 +61,18 @@ const controller = {
61
61
  });
62
62
  },
63
63
  delete() {
64
- return Promise.resolve(function destroySessionMw(req, res, next) {
65
- auth.session.destroySession(req, res, next);
64
+ return Promise.resolve(function logoutSessionMw(req, res, next) {
65
+ auth.session.logout(req, res, next);
66
+ });
67
+ },
68
+ sendVerification() {
69
+ return Promise.resolve(function sendAuthCodeMw(req, res, next) {
70
+ auth.session.sendAuthCode(req, res, next);
71
+ });
72
+ },
73
+ verify() {
74
+ return Promise.resolve(function verifyAuthCodeMw(req, res, next) {
75
+ auth.session.verifyAuthCode(req, res, next);
66
76
  });
67
77
  }
68
78
  };
@@ -0,0 +1,25 @@
1
+ const logging = require('@tryghost/logging');
2
+
3
+ const {createTransactionalMigration} = require('../../utils');
4
+
5
+ module.exports = createTransactionalMigration(
6
+ async function up(knex) {
7
+ logging.info('Adding verified property to sessions');
8
+
9
+ await knex.raw(`
10
+ UPDATE sessions
11
+ SET session_data = JSON_SET(session_data, '$.verified', 'true')
12
+ WHERE JSON_VALID(session_data);
13
+ `);
14
+ },
15
+
16
+ async function down(knex) {
17
+ logging.info('Removing verified property from sessions');
18
+
19
+ await knex.raw(`
20
+ UPDATE sessions
21
+ SET session_data = JSON_REMOVE(session_data, '$.verified')
22
+ WHERE JSON_VALID(session_data);
23
+ `);
24
+ }
25
+ );
@@ -202,6 +202,21 @@ const Comment = ghostBookshelf.Model.extend({
202
202
  return options;
203
203
  },
204
204
 
205
+ async findMostLikedComment(options = {}) {
206
+ let query = ghostBookshelf.knex('comments')
207
+ .select('comments.*')
208
+ .count('comment_likes.id as count__likes') // Counting likes for sorting
209
+ .leftJoin('comment_likes', 'comments.id', 'comment_likes.comment_id')
210
+ .groupBy('comments.id') // Group by comment ID to aggregate likes count
211
+ .orderBy('count__likes', 'desc') // Order by likes in descending order (most likes first)
212
+ .limit(1); // Limit to just 1 result
213
+ // Execute the query and get the result
214
+ const result = await query.first(); // Fetch the single top comment
215
+ const id = result && result.id;
216
+ // Fetch the comment model by ID
217
+ return this.findOne({id}, options);
218
+ },
219
+
205
220
  async findPage(options) {
206
221
  const {withRelated} = this.defaultRelations('findPage', options);
207
222
 
@@ -218,6 +233,16 @@ const Comment = ghostBookshelf.Model.extend({
218
233
  await model.load(relationsToLoadIndividually, _.omit(options, 'withRelated'));
219
234
  }
220
235
 
236
+ // if options.order === 'best', we findMostLikedComment
237
+ // then we remove it from the result set and add it as the first element
238
+ if (options.order === 'best' && options.page === '1') {
239
+ const mostLikedComment = await this.findMostLikedComment(options);
240
+ if (mostLikedComment) {
241
+ result.data = result.data.filter(comment => comment.id !== mostLikedComment.id);
242
+ result.data.unshift(mostLikedComment);
243
+ }
244
+ }
245
+
221
246
  return result;
222
247
  },
223
248
 
@@ -1,5 +1,4 @@
1
1
  const {Store} = require('express-session');
2
- const {InternalServerError} = require('@tryghost/errors');
3
2
 
4
3
  module.exports = class SessionStore extends Store {
5
4
  constructor(SessionModel) {
@@ -29,11 +28,6 @@ module.exports = class SessionStore extends Store {
29
28
  }
30
29
 
31
30
  set(sid, sessionData, callback) {
32
- if (!sessionData.user_id) {
33
- return callback(new InternalServerError({
34
- message: 'Cannot create a session with no user_id'
35
- }));
36
- }
37
31
  this.SessionModel
38
32
  .upsert({session_data: sessionData}, {session_id: sid})
39
33
  .then(() => {
@@ -2,13 +2,21 @@ const adapterManager = require('../../adapter-manager');
2
2
  const createSessionService = require('@tryghost/session-service');
3
3
  const sessionFromToken = require('@tryghost/mw-session-from-token');
4
4
  const createSessionMiddleware = require('./middleware');
5
+ const settingsCache = require('../../../../shared/settings-cache');
6
+ const {GhostMailer} = require('../../mail');
7
+ const {t} = require('../../i18n');
8
+ const labs = require('../../../../shared/labs');
5
9
 
6
10
  const expressSession = require('./express-session');
7
11
 
8
12
  const models = require('../../../models');
9
13
  const urlUtils = require('../../../../shared/url-utils');
14
+ const {blogIcon} = require('../../../lib/image');
10
15
  const url = require('url');
11
16
 
17
+ // TODO: We have too many lines here, should move functions out into a utils module
18
+ /* eslint-disable max-lines */
19
+
12
20
  function getOriginOfRequest(req) {
13
21
  const origin = req.get('origin');
14
22
  const referrer = req.get('referrer') || urlUtils.getAdminUrl() || urlUtils.getSiteUrl();
@@ -28,12 +36,25 @@ function getOriginOfRequest(req) {
28
36
  return null;
29
37
  }
30
38
 
39
+ const mailer = new GhostMailer();
40
+
31
41
  const sessionService = createSessionService({
32
42
  getOriginOfRequest,
33
43
  getSession: expressSession.getSession,
34
44
  findUserById({id}) {
35
45
  return models.User.findOne({id, status: 'active'});
36
- }
46
+ },
47
+ getSettingsCache(key) {
48
+ return settingsCache.get(key);
49
+ },
50
+ getBlogLogo() {
51
+ return blogIcon.getIconUrl({absolute: true, fallbackToDefault: false})
52
+ || 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png';
53
+ },
54
+ mailer,
55
+ urlUtils,
56
+ labs,
57
+ t
37
58
  });
38
59
 
39
60
  module.exports = createSessionMiddleware({sessionService});
@@ -1,19 +1,36 @@
1
+ const errors = require('@tryghost/errors');
2
+
1
3
  function SessionMiddleware({sessionService}) {
2
4
  async function createSession(req, res, next) {
3
5
  try {
4
6
  await sessionService.createSessionForUser(req, res, req.user);
5
- res.sendStatus(201);
7
+
8
+ const isVerified = await sessionService.isVerifiedSession(req, res);
9
+ if (isVerified) {
10
+ res.sendStatus(201);
11
+ } else {
12
+ await sessionService.sendAuthCodeToUser(req, res);
13
+ throw new errors.NoPermissionError({
14
+ code: '2FA_TOKEN_REQUIRED',
15
+ errorType: 'Needs2FAError',
16
+ message: 'User must verify session to login.'
17
+ });
18
+ }
6
19
  } catch (err) {
7
20
  next(err);
8
21
  }
9
22
  }
10
23
 
11
- async function destroySession(req, res, next) {
24
+ async function logout(req, res, next) {
12
25
  try {
13
- await sessionService.destroyCurrentSession(req);
26
+ await sessionService.removeUserForSession(req, res);
14
27
  res.sendStatus(204);
15
28
  } catch (err) {
16
- next(err);
29
+ if (errors.utils.isGhostError(err)) {
30
+ next(err);
31
+ } else {
32
+ next(new errors.InternalServerError({err}));
33
+ }
17
34
  }
18
35
  }
19
36
 
@@ -21,6 +38,11 @@ function SessionMiddleware({sessionService}) {
21
38
  try {
22
39
  const user = await sessionService.getUserForSession(req, res);
23
40
  if (user) {
41
+ const isVerified = await sessionService.isVerifiedSession(req, res);
42
+ if (!isVerified) {
43
+ return next();
44
+ }
45
+
24
46
  // Do not nullify `req.user` as it might have been already set
25
47
  // in a previous middleware (authorize middleware).
26
48
  req.user = user;
@@ -31,10 +53,37 @@ function SessionMiddleware({sessionService}) {
31
53
  }
32
54
  }
33
55
 
56
+ async function sendAuthCode(req, res, next) {
57
+ try {
58
+ await sessionService.sendAuthCodeToUser(req, res);
59
+
60
+ res.sendStatus(200);
61
+ } catch (err) {
62
+ next(err);
63
+ }
64
+ }
65
+
66
+ async function verifyAuthCode(req, res, next) {
67
+ try {
68
+ const verified = await sessionService.verifyAuthCodeForUser(req, res);
69
+
70
+ if (verified) {
71
+ await sessionService.verifySession(req, res);
72
+ res.sendStatus(200);
73
+ } else {
74
+ res.sendStatus(401);
75
+ }
76
+ } catch (err) {
77
+ next(err);
78
+ }
79
+ }
80
+
34
81
  return {
35
82
  createSession: createSession,
36
- destroySession: destroySession,
37
- authenticate: authenticate
83
+ logout: logout,
84
+ authenticate: authenticate,
85
+ sendAuthCode: sendAuthCode,
86
+ verifyAuthCode: verifyAuthCode
38
87
  };
39
88
  }
40
89
 
@@ -93,7 +93,7 @@ class Queue extends EventEmitter {
93
93
  };
94
94
  }
95
95
 
96
- debug('add', options.event, options.tolerance);
96
+ debug('register', options.event, options.tolerance);
97
97
 
98
98
  this.queue[options.event].subscribers.push(fn);
99
99
  }
@@ -103,19 +103,20 @@ class Queue extends EventEmitter {
103
103
  * @param {Object} options
104
104
  */
105
105
  run(options) {
106
- const event = options.event;
107
- const action = options.action;
108
- const eventData = options.eventData;
106
+ const {event, action, eventData} = options;
109
107
 
110
108
  clearTimeout(this.toNotify[action].timeout);
111
109
  this.toNotify[action].timeout = null;
112
110
 
113
- debug('run', action, event, this.queue[event].subscribers.length, this.toNotify[action].notified.length);
111
+ const subscribers = this.queue[event].subscribers;
112
+ const notified = this.toNotify[action].notified;
114
113
 
115
- if (this.queue[event].subscribers.length && this.queue[event].subscribers.length !== this.toNotify[action].notified.length) {
116
- const fn = this.queue[event].subscribers[this.toNotify[action].notified.length];
114
+ debug('run', action, event, subscribers.length, notified.length);
117
115
 
118
- debug('execute', action, event, this.toNotify[action].notified.length);
116
+ if (subscribers.length && subscribers.length !== notified.length) {
117
+ const fn = subscribers[notified.length];
118
+
119
+ debug('run.execute', action, event, notified.length);
119
120
 
120
121
  /**
121
122
  * @NOTE: Currently no async operations happen in the subscribers functions.
@@ -124,7 +125,7 @@ class Queue extends EventEmitter {
124
125
  try {
125
126
  fn(eventData);
126
127
 
127
- debug('executed', action, event, this.toNotify[action].notified.length);
128
+ debug('run.executed', action, event, notified.length);
128
129
  this.toNotify[action].notified.push(fn);
129
130
  this.run(options);
130
131
  } catch (err) {
@@ -144,15 +145,15 @@ class Queue extends EventEmitter {
144
145
  // CASE 3: wait for more subscribers, i am still tolerant
145
146
  if (this.queue[event].tolerance === 0) {
146
147
  delete this.toNotify[action];
147
- debug('ended (1)', event, action);
148
+ debug('run.ended (1)', event, action);
148
149
  this.emit('ended', event);
149
- } else if (this.queue[options.event].subscribers.length >= this.queue[options.event].requiredSubscriberCount &&
150
+ } else if (subscribers.length >= this.queue[event].requiredSubscriberCount &&
150
151
  this.toNotify[action].timeoutInMS > this.queue[event].tolerance) {
151
152
  delete this.toNotify[action];
152
- debug('ended (2)', event, action);
153
+ debug('run.ended (2)', event, action);
153
154
  this.emit('ended', event);
154
155
  } else {
155
- debug('retry', event, action, this.toNotify[action].timeoutInMS);
156
+ debug('run.retry', event, action, this.toNotify[action].timeoutInMS);
156
157
 
157
158
  this.toNotify[action].timeoutInMS = this.toNotify[action].timeoutInMS * 1.1;
158
159
 
@@ -128,12 +128,13 @@ class Resources {
128
128
  modelOptions.limit = options.limit;
129
129
  }
130
130
 
131
+ const now = Date.now();
131
132
  const objects = await models.Base.Model.raw_knex.fetchAll(modelOptions);
132
- debug('fetched', resourceConfig.type, objects.length);
133
+ debug('_fetch.fetched', resourceConfig.type, objects.length, `${Date.now() - now}ms`);
133
134
 
134
- _.each(objects, (object) => {
135
+ for (const object of objects) {
135
136
  this.data[resourceConfig.type].push(new Resource(resourceConfig.type, object));
136
- });
137
+ }
137
138
 
138
139
  if (objects.length && isSQLite) {
139
140
  options.offset = options.offset + options.limit;
@@ -1,4 +1,3 @@
1
- const _ = require('lodash');
2
1
  const nql = require('@tryghost/nql');
3
2
  const debug = require('@tryghost/debug')('services:url:generator');
4
3
  const localUtils = require('../../../shared/url-utils');
@@ -121,16 +120,14 @@ class UrlGenerator {
121
120
  * @private
122
121
  */
123
122
  _onInit() {
124
- debug('_onInit', this.resourceType);
125
-
126
123
  // @NOTE: get the resources of my type e.g. posts.
127
124
  const resources = this.resources.getAllByType(this.resourceType);
128
125
 
129
- debug(resources.length);
126
+ debug('_onInit', this.resourceType, resources.length);
130
127
 
131
- _.each(resources, (resource) => {
128
+ for (const resource of resources) {
132
129
  this._try(resource);
133
- });
130
+ }
134
131
  }
135
132
 
136
133
  /**
@@ -141,7 +138,7 @@ class UrlGenerator {
141
138
  * @private
142
139
  */
143
140
  _onAdded(event) {
144
- debug('onAdded', this.toString());
141
+ debug('_onAdded', this.toString());
145
142
 
146
143
  // CASE: you are type "pages", but the incoming type is "users"
147
144
  if (event.type !== this.resourceType) {
@@ -149,7 +146,6 @@ class UrlGenerator {
149
146
  }
150
147
 
151
148
  const resource = this.resources.getByIdAndType(event.type, event.id);
152
-
153
149
  this._try(resource);
154
150
  }
155
151
 
@@ -1,5 +1,4 @@
1
- const _debug = require('@tryghost/debug')._base;
2
- const debug = _debug('ghost:services:url:service');
1
+ const debug = require('@tryghost/debug')('services:url:service');
3
2
  const _ = require('lodash');
4
3
  const errors = require('@tryghost/errors');
5
4
  const labs = require('../../../shared/labs');
@@ -92,7 +91,7 @@ class UrlService {
92
91
  * @param {String} permalink
93
92
  */
94
93
  onRouterAddedType(identifier, filter, resourceType, permalink) {
95
- debug('Registering route: ', filter, resourceType, permalink);
94
+ debug('Registering route:', filter, resourceType, permalink);
96
95
 
97
96
  let urlGenerator = new UrlGenerator({
98
97
  identifier,
@@ -37,11 +37,8 @@ class Urls {
37
37
  * @param {string} options.url
38
38
  */
39
39
  add(options) {
40
- const url = options.url;
41
- const generatorId = options.generatorId;
42
- const resource = options.resource;
43
-
44
- debug('cache', url);
40
+ const {url, generatorId, resource} = options;
41
+ debug('add', resource.data.id, url);
45
42
 
46
43
  if (this.urls[resource.data.id]) {
47
44
  const error = new errors.InternalServerError({
@@ -131,7 +128,7 @@ class Urls {
131
128
  return;
132
129
  }
133
130
 
134
- debug('removed', this.urls[id].url, this.urls[id].generatorId);
131
+ debug('removeResourceId', this.urls[id].url, this.urls[id].generatorId);
135
132
 
136
133
  events.emit('url.removed', {
137
134
  url: this.urls[id].url,
@@ -243,6 +243,8 @@ module.exports = function apiRoutes() {
243
243
  http(api.session.add)
244
244
  );
245
245
  router.del('/session', mw.authAdminApi, http(api.session.delete));
246
+ router.post('/session/verify', shared.middleware.brute.sendVerificationCode, http(api.session.sendVerification));
247
+ router.put('/session/verify', shared.middleware.brute.userVerification, http(api.session.verify));
246
248
 
247
249
  // ## Identity
248
250
  router.get('/identities', mw.authAdminApi, http(api.identities.read));