ghost 4.18.0 → 4.20.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 (237) hide show
  1. package/.eslintrc.js +9 -8
  2. package/Gruntfile.js +1 -1
  3. package/PRIVACY.md +3 -0
  4. package/content/adapters/README.md +2 -2
  5. package/core/boot.js +17 -12
  6. package/core/bridge.js +9 -1
  7. package/core/built/assets/{chunk.3.b80d3e1e6b8556aaff3c.js → chunk.3.777d43e2ce954ba8b2f5.js} +25 -25
  8. package/core/built/assets/codemirror/{codemirror-21a09582262987037db73b152fb35f7c.js → codemirror-d25c379b87ec8b33d54ac7149bc0b6ae.js} +14 -14
  9. package/core/built/assets/ghost-dark-20e2892d4f30d0d1183c9ac725ea37d0.css +1 -0
  10. package/core/built/assets/{ghost.min-88d647a008a5b1dd678a89ae1e55c038.js → ghost.min-26e427944e719b616b8dc7fbb3bbd2f9.js} +709 -422
  11. package/core/built/assets/ghost.min-57e46fd3b1145ecf2cbd185a13611f3b.css +1 -0
  12. package/core/built/assets/icons/arrow-left-small.svg +0 -4
  13. package/core/built/assets/icons/paintbrush.svg +10 -1
  14. package/core/built/assets/icons/post.svg +3 -1
  15. package/core/built/assets/img/footer-marketplace-bg-572b6c6486a7e26316954d599eaa9f30.png +0 -0
  16. package/core/built/assets/img/marketing/offers-1-f2e1b653c4d5bb90eea9d7a2862530f9.jpg +0 -0
  17. package/core/built/assets/img/marketing/offers-2-28a225d34cc39d133748431536961d00.jpg +0 -0
  18. package/core/built/assets/img/marketing/offers-3-2094c91ab21a16c37fbe6ec16c140160.jpg +0 -0
  19. package/core/built/assets/img/themes/Alto-f4db5af43ca9771c7ac1f754de3ddf2f.png +0 -0
  20. package/core/built/assets/img/themes/Bulletin-57d45b992ff0e26e0acdce7ed4cccd67.png +0 -0
  21. package/core/built/assets/img/themes/Casper-c7e784d7188cc5d7f097d9b6c97b0263.jpg +0 -0
  22. package/core/built/assets/img/themes/Dawn-be81aa8c8caae8fcfb5d5fbec823fdcc.png +0 -0
  23. package/core/built/assets/img/themes/Digest-d3467ac22a290e1ad3a543014758286e.png +0 -0
  24. package/core/built/assets/img/themes/Dope-6f8e0bbc199ce4af9a60859e9e6a74ad.png +0 -0
  25. package/core/built/assets/img/themes/Ease-9c279ea6cec3c0f1823f81c9dd24b116.png +0 -0
  26. package/core/built/assets/img/themes/Edge-0258906309e11fd075a1d9880aa09b20.png +0 -0
  27. package/core/built/assets/img/themes/Edition-d8f508e93bc24bdf2716ae6f8b3d44f8.png +0 -0
  28. package/core/built/assets/img/themes/Editorial-a25a4a34c04dedd858bd5e05ef388b1c.jpg +0 -0
  29. package/core/built/assets/img/themes/Journal-accf0031bbae0919900a049061e65a04.png +0 -0
  30. package/core/built/assets/img/themes/London-3f07efcee9e5bfb9a33827064eb77e70.jpg +0 -0
  31. package/core/built/assets/img/themes/Massively-06edf00108429f7fb8e65f190fba34fe.jpg +0 -0
  32. package/core/built/assets/img/themes/Ruby-11a53c62015612f4b3aca8f503121225.png +0 -0
  33. package/core/built/assets/img/themes/Wave-86e8044c2d76cb57a9030e4c24ac9520.png +0 -0
  34. package/core/built/assets/simplemde/{simplemde-232f69d126310434489071a1891e6d8b.js → simplemde-3ffc0ec9e9fecf29b9a499db678c9e65.js} +14 -14
  35. package/core/built/assets/{vendor.min-7dc7cf9c92175ebfb9cea95c120ee8a7.js → vendor.min-af502ac4142871500fc424f6a5a254ec.js} +2206 -1859
  36. package/core/frontend/apps/amp/lib/router.js +1 -1
  37. package/core/frontend/helpers/match.js +17 -23
  38. package/core/frontend/meta/author-url.js +1 -1
  39. package/core/frontend/meta/url.js +1 -1
  40. package/core/{server → frontend}/public/favicon.ico +0 -0
  41. package/core/{server → frontend}/public/ghost.css +0 -0
  42. package/core/{server → frontend}/public/ghost.min.css +0 -0
  43. package/core/{server → frontend}/public/robots.txt +0 -0
  44. package/core/{server → frontend}/public/sitemap.xsl +0 -0
  45. package/core/frontend/services/proxy.js +1 -1
  46. package/core/frontend/services/rendering.js +1 -1
  47. package/core/frontend/services/routing/CollectionRouter.js +3 -49
  48. package/core/frontend/services/routing/ParentRouter.js +1 -4
  49. package/core/frontend/services/routing/StaticPagesRouter.js +3 -5
  50. package/core/frontend/services/routing/StaticRoutesRouter.js +4 -6
  51. package/core/frontend/services/routing/TaxonomyRouter.js +4 -5
  52. package/core/frontend/services/routing/controllers/collection.js +2 -2
  53. package/core/frontend/services/routing/controllers/email-post.js +2 -2
  54. package/core/frontend/services/routing/controllers/entry.js +2 -2
  55. package/core/frontend/services/routing/controllers/preview.js +2 -2
  56. package/core/frontend/services/routing/index.js +6 -12
  57. package/core/frontend/services/routing/registry.js +13 -0
  58. package/core/frontend/services/routing/router-manager.js +185 -0
  59. package/core/frontend/services/rss/generate-feed.js +2 -2
  60. package/core/frontend/services/theme-engine/i18n/i18n.js +267 -28
  61. package/core/frontend/services/theme-engine/i18n/index.js +1 -1
  62. package/core/frontend/services/theme-engine/i18n/theme-i18n.js +73 -0
  63. package/core/frontend/web/index.js +1 -0
  64. package/core/{server/web/site → frontend/web}/middleware/handle-image-sizes.js +4 -4
  65. package/core/{server/web/site → frontend/web}/middleware/index.js +0 -0
  66. package/core/{server/web/site → frontend/web}/middleware/redirect-ghost-to-admin.js +3 -3
  67. package/core/{server/web/site → frontend/web}/middleware/serve-favicon.js +6 -6
  68. package/core/{server/web/site → frontend/web}/middleware/serve-public-file.js +2 -2
  69. package/core/{server/web/site → frontend/web}/middleware/static-theme.js +3 -3
  70. package/core/frontend/web/routes.js +13 -0
  71. package/core/{server/web/site/app.js → frontend/web/site.js} +12 -16
  72. package/core/server/adapters/storage/LocalFileStorage.js +35 -39
  73. package/core/server/adapters/storage/index.js +12 -2
  74. package/core/server/api/canary/custom-theme-settings.js +2 -2
  75. package/core/server/api/canary/images.js +1 -1
  76. package/core/server/api/canary/oembed.js +2 -2
  77. package/core/server/api/canary/offers.js +29 -1
  78. package/core/server/api/canary/posts-public.js +6 -2
  79. package/core/server/api/canary/products.js +6 -2
  80. package/core/server/api/canary/tags-public.js +6 -2
  81. package/core/server/api/canary/users.js +9 -4
  82. package/core/server/api/canary/utils/serializers/output/custom-theme-settings.js +2 -2
  83. package/core/server/api/canary/utils/serializers/output/notifications.js +1 -0
  84. package/core/server/api/canary/utils/serializers/output/settings.js +2 -3
  85. package/core/server/api/canary/utils/serializers/output/utils/url.js +1 -1
  86. package/core/server/api/canary/utils/validators/input/oembed.js +4 -1
  87. package/core/server/api/canary/utils/validators/input/passwordreset.js +8 -3
  88. package/core/server/api/canary/utils/validators/input/settings.js +5 -4
  89. package/core/server/api/canary/utils/validators/input/setup.js +6 -2
  90. package/core/server/api/canary/utils/validators/input/users.js +6 -2
  91. package/core/server/api/canary/utils/validators/input/webhooks.js +8 -3
  92. package/core/server/api/v2/images.js +1 -1
  93. package/core/server/api/v2/utils/serializers/output/authentication.js +9 -4
  94. package/core/server/api/v2/utils/serializers/output/notifications.js +1 -0
  95. package/core/server/api/v2/utils/serializers/output/users.js +5 -3
  96. package/core/server/api/v2/utils/serializers/output/utils/url.js +1 -1
  97. package/core/server/api/v2/utils/validators/input/images.js +11 -6
  98. package/core/server/api/v2/utils/validators/input/invitations.js +14 -6
  99. package/core/server/api/v2/utils/validators/input/invites.js +6 -2
  100. package/core/server/api/v2/utils/validators/input/oembed.js +6 -2
  101. package/core/server/api/v2/utils/validators/input/passwordreset.js +8 -3
  102. package/core/server/api/v2/utils/validators/input/settings.js +10 -4
  103. package/core/server/api/v2/utils/validators/input/setup.js +6 -2
  104. package/core/server/api/v2/utils/validators/input/users.js +5 -2
  105. package/core/server/api/v3/authentication.js +6 -2
  106. package/core/server/api/v3/authors-public.js +6 -2
  107. package/core/server/api/v3/email.js +9 -4
  108. package/core/server/api/v3/images.js +1 -1
  109. package/core/server/api/v3/integrations.js +7 -3
  110. package/core/server/api/v3/invites.js +6 -3
  111. package/core/server/api/v3/labels.js +10 -5
  112. package/core/server/api/v3/memberSigninUrls.js +5 -2
  113. package/core/server/api/v3/oembed.js +2 -2
  114. package/core/server/api/v3/pages-public.js +5 -2
  115. package/core/server/api/v3/pages.js +6 -3
  116. package/core/server/api/v3/posts-public.js +5 -3
  117. package/core/server/api/v3/posts.js +7 -3
  118. package/core/server/api/v3/preview.js +5 -3
  119. package/core/server/api/v3/session.js +7 -3
  120. package/core/server/api/v3/settings.js +8 -3
  121. package/core/server/api/v3/slugs.js +5 -4
  122. package/core/server/api/v3/utils/serializers/output/authentication.js +10 -4
  123. package/core/server/api/v3/utils/serializers/output/notifications.js +1 -0
  124. package/core/server/api/v3/utils/serializers/output/settings.js +2 -3
  125. package/core/server/api/v3/utils/serializers/output/users.js +6 -2
  126. package/core/server/api/v3/utils/serializers/output/utils/url.js +1 -1
  127. package/core/server/api/v3/utils/validators/input/images.js +12 -7
  128. package/core/server/api/v3/utils/validators/input/invitations.js +14 -6
  129. package/core/server/api/v3/utils/validators/input/invites.js +6 -2
  130. package/core/server/api/v3/utils/validators/input/oembed.js +6 -2
  131. package/core/server/api/v3/utils/validators/input/passwordreset.js +8 -3
  132. package/core/server/api/v3/utils/validators/input/settings.js +5 -4
  133. package/core/server/api/v3/utils/validators/input/setup.js +6 -2
  134. package/core/server/api/v3/utils/validators/input/users.js +6 -2
  135. package/core/server/api/v3/utils/validators/input/webhooks.js +8 -3
  136. package/core/server/data/exporter/table-lists.js +2 -1
  137. package/core/server/data/importer/handlers/image.js +1 -1
  138. package/core/server/data/importer/importers/image.js +1 -1
  139. package/core/server/data/migrations/init/1-create-tables.js +7 -8
  140. package/core/server/data/migrations/init/2-create-fixtures.js +8 -8
  141. package/core/server/data/migrations/versions/4.19/01-add-active-column-to-offers.js +7 -0
  142. package/core/server/data/migrations/versions/4.19/02-add-offer-redemptions-table.js +8 -0
  143. package/core/server/data/migrations/versions/4.20/01-remove-offer-redemptions-table.js +19 -0
  144. package/core/server/data/migrations/versions/4.20/02-remove-offers-table.js +30 -0
  145. package/core/server/data/migrations/versions/4.20/03-add-offers-table.js +21 -0
  146. package/core/server/data/migrations/versions/4.20/04-add-offer-redemptions-table.js +9 -0
  147. package/core/server/data/migrations/versions/4.20/05-remove-not-null-constraint-from-portal-title.js +41 -0
  148. package/core/server/data/schema/fixtures/utils.js +150 -143
  149. package/core/server/data/schema/schema.js +15 -3
  150. package/core/server/frontend/ghost.min.css +1 -0
  151. package/core/server/lib/image/blog-icon.js +10 -10
  152. package/core/server/lib/image/image-size.js +5 -5
  153. package/core/server/lib/image/image-utils.js +4 -4
  154. package/core/server/lib/image/index.js +1 -2
  155. package/core/server/lib/mobiledoc.js +3 -2
  156. package/core/server/models/action.js +7 -4
  157. package/core/server/models/base/plugins/overrides.js +19 -6
  158. package/core/server/models/custom-theme-setting.js +56 -1
  159. package/core/server/models/index.js +4 -45
  160. package/core/server/models/member.js +5 -0
  161. package/core/server/models/offer-redemption.js +10 -0
  162. package/core/server/models/user.js +2 -1
  163. package/core/server/overrides.js +6 -2
  164. package/core/server/run-update-check.js +0 -3
  165. package/core/server/services/adapter-manager/config.js +1 -0
  166. package/core/server/services/adapter-manager/index.js +9 -5
  167. package/core/server/services/adapter-manager/options-resolver.js +18 -0
  168. package/core/server/services/bulk-email/bulk-email-processor.js +6 -2
  169. package/core/server/services/bulk-email/mailgun.js +1 -1
  170. package/core/server/services/custom-theme-settings.js +10 -4
  171. package/core/server/services/invites/index.js +0 -2
  172. package/core/server/services/invites/invites.js +5 -5
  173. package/core/server/services/mail/GhostMailer.js +18 -10
  174. package/core/server/services/mega/mega.js +3 -3
  175. package/core/server/services/mega/post-email-serializer.js +2 -2
  176. package/core/server/services/members/api.js +3 -4
  177. package/core/server/services/members/emails/signin.js +1 -1
  178. package/core/server/services/members/emails/signup.js +1 -1
  179. package/core/server/services/members/emails/subscribe.js +1 -1
  180. package/core/server/services/members/middleware.js +10 -0
  181. package/core/server/services/members/service.js +2 -1
  182. package/core/server/services/notifications/index.js +1 -1
  183. package/core/server/services/notifications/notifications.js +40 -35
  184. package/core/server/services/oembed.js +4 -9
  185. package/core/server/services/offers/service.js +16 -6
  186. package/core/server/services/permissions/public.js +6 -2
  187. package/core/server/services/route-settings/route-settings.js +1 -1
  188. package/core/server/services/settings/index.js +3 -1
  189. package/core/server/services/settings/settings-bread-service.js +42 -20
  190. package/core/server/services/slack.js +1 -1
  191. package/core/server/services/themes/activate.js +2 -2
  192. package/core/server/services/themes/activation-bridge.js +6 -6
  193. package/core/server/services/themes/storage.js +1 -1
  194. package/core/{frontend → server}/services/url/Queue.js +0 -0
  195. package/core/{frontend → server}/services/url/Resource.js +0 -0
  196. package/core/{frontend → server}/services/url/Resources.js +2 -2
  197. package/core/{frontend → server}/services/url/UrlGenerator.js +14 -14
  198. package/core/{frontend → server}/services/url/UrlService.js +12 -15
  199. package/core/{frontend → server}/services/url/Urls.js +1 -1
  200. package/core/{frontend → server}/services/url/configs/canary.js +0 -0
  201. package/core/{frontend → server}/services/url/configs/v2.js +0 -0
  202. package/core/{frontend → server}/services/url/configs/v3.js +0 -0
  203. package/core/{frontend → server}/services/url/configs/v4.js +0 -0
  204. package/core/{frontend → server}/services/url/index.js +0 -0
  205. package/core/server/services/xmlrpc.js +1 -1
  206. package/core/server/update-check.js +3 -3
  207. package/core/server/web/admin/controller.js +11 -0
  208. package/core/server/web/admin/views/default-prod.html +4 -4
  209. package/core/server/web/admin/views/default.html +4 -4
  210. package/core/server/web/api/app.js +8 -9
  211. package/core/server/web/members/app.js +1 -0
  212. package/core/server/web/oauth/app.js +4 -2
  213. package/core/server/web/parent/backend.js +3 -3
  214. package/core/server/web/parent/frontend.js +2 -2
  215. package/core/server/web/shared/middlewares/api/spam-prevention.js +0 -2
  216. package/core/server/web/shared/middlewares/custom-redirects.js +0 -8
  217. package/core/server/web/shared/middlewares/maintenance.js +1 -1
  218. package/core/server/web/well-known.js +10 -10
  219. package/core/shared/config/defaults.json +2 -2
  220. package/core/shared/config/overrides.json +1 -1
  221. package/core/shared/express.js +10 -0
  222. package/core/shared/html-to-plaintext.js +2 -2
  223. package/core/shared/labs.js +14 -6
  224. package/loggingrc.js +10 -0
  225. package/package.json +60 -57
  226. package/yarn.lock +1317 -914
  227. package/core/built/assets/ghost-dark-13627f10941a7dbb2b12e1d41dc51c34.css +0 -1
  228. package/core/built/assets/ghost.min-d9cbfb4eb2db8915fcd2bf2416218616.css +0 -1
  229. package/core/built/assets/img/themes/London-68501c8ab797de7f2851cf9ea0a28e26.jpg +0 -0
  230. package/core/frontend/services/routing/bootstrap.js +0 -114
  231. package/core/server/public/404-ghost.png +0 -0
  232. package/core/server/public/404-ghost@2x.png +0 -0
  233. package/core/server/web/site/index.js +0 -1
  234. package/core/server/web/site/routes.js +0 -9
  235. package/core/shared/i18n/i18n.js +0 -312
  236. package/core/shared/i18n/index.js +0 -6
  237. package/core/shared/i18n/translations/en.json +0 -675
@@ -70,7 +70,6 @@ function createApiInstance(config) {
70
70
  For your security, the link will expire in 24 hours time.
71
71
 
72
72
  All the best!
73
- The team at ${siteTitle}
74
73
 
75
74
  ---
76
75
 
@@ -88,7 +87,6 @@ function createApiInstance(config) {
88
87
  For your security, the link will expire in 24 hours time.
89
88
 
90
89
  See you soon!
91
- The team at ${siteTitle}
92
90
 
93
91
  ---
94
92
 
@@ -122,7 +120,6 @@ function createApiInstance(config) {
122
120
  For your security, the link will expire in 24 hours time.
123
121
 
124
122
  See you soon!
125
- The team at ${siteTitle}
126
123
 
127
124
  ---
128
125
 
@@ -180,6 +177,8 @@ function createApiInstance(config) {
180
177
  MemberStatusEvent: models.MemberStatusEvent,
181
178
  MemberProductEvent: models.MemberProductEvent,
182
179
  MemberAnalyticEvent: models.MemberAnalyticEvent,
180
+ OfferRedemption: models.OfferRedemption,
181
+ Offer: models.Offer,
183
182
  StripeProduct: models.StripeProduct,
184
183
  StripePrice: models.StripePrice,
185
184
  Product: models.Product,
@@ -187,7 +186,7 @@ function createApiInstance(config) {
187
186
  },
188
187
  stripeAPIService: stripeService.api,
189
188
  logger: logging,
190
- offerRepository: offersService.repository,
189
+ offersAPI: offersService.api,
191
190
  labsService: labsService
192
191
  });
193
192
 
@@ -134,7 +134,7 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
134
134
  </tbody>
135
135
  </table>
136
136
  <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 25px;">For your security, the link will expire in 24 hours time.</p>
137
- <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 30px;">See you soon!<br/>The team at ${siteTitle}</p>
137
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 30px;">See you soon!</p>
138
138
  <hr/>
139
139
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
140
140
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin-top:0; color: #3A464C;">${url}</p>
@@ -134,7 +134,7 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
134
134
  </tbody>
135
135
  </table>
136
136
  <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 25px;">For your security, the link will expire in 24 hours time.</p>
137
- <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 30px;">See you soon!<br/>The team at ${siteTitle}</p>
137
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 30px;">See you soon!</p>
138
138
  <hr/>
139
139
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; line-height: 25px; margin: 0; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
140
140
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin-top: 0; color: #3A464C;">${url}</p>
@@ -134,7 +134,7 @@ module.exports = ({siteTitle, email, url, accentColor = '#15212A', siteDomain, s
134
134
  </tbody>
135
135
  </table>
136
136
  <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 25px;">For your security, the link will expire in 24 hours time.</p>
137
- <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 30px;">All the best!<br/>The team at ${siteTitle}</p>
137
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 30px;">All the best!</p>
138
138
  <hr/>
139
139
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
140
140
  <p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin-top: 0; color: #3A464C;">${url}</p>
@@ -1,6 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const logging = require('@tryghost/logging');
3
3
  const membersService = require('./service');
4
+ const offersService = require('../offers/service');
4
5
  const urlUtils = require('../../../shared/url-utils');
5
6
  const ghostVersion = require('@tryghost/version');
6
7
  const settingsCache = require('../../../shared/settings-cache');
@@ -58,6 +59,14 @@ const getMemberData = async function (req, res) {
58
59
  }
59
60
  };
60
61
 
62
+ const getOfferData = async function (req, res) {
63
+ const offerId = req.params.id;
64
+ const offer = await offersService.api.getOffer({id: offerId});
65
+ return res.json({
66
+ offers: [offer]
67
+ });
68
+ };
69
+
61
70
  const updateMemberData = async function (req, res) {
62
71
  try {
63
72
  const data = _.pick(req.body, 'name', 'subscribed');
@@ -222,6 +231,7 @@ module.exports = {
222
231
  createSessionFromMagicLink,
223
232
  getIdentityToken,
224
233
  getMemberData,
234
+ getOfferData,
225
235
  updateMemberData,
226
236
  getMemberSiteData,
227
237
  deleteSession,
@@ -61,7 +61,8 @@ function reconfigureMembersAPI() {
61
61
  */
62
62
  const fetchImportThreshold = async () => {
63
63
  const membersTotal = await membersService.stats.getTotalMembers();
64
- const volumeThreshold = _.get(config.get('hostSettings'), 'emailVerification.importThreshold') || Infinity;
64
+ const configThreshold = _.get(config.get('hostSettings'), 'emailVerification.importThreshold');
65
+ const volumeThreshold = (configThreshold === undefined) ? Infinity : configThreshold;
65
66
  const threshold = Math.max(membersTotal, volumeThreshold);
66
67
 
67
68
  return threshold;
@@ -5,6 +5,6 @@ const models = require('../../models');
5
5
 
6
6
  module.exports.notifications = new Notifications({
7
7
  settingsCache,
8
- ghostVersion,
8
+ ghostVersion: ghostVersion.full,
9
9
  SettingsModel: models.Settings
10
10
  });
@@ -16,8 +16,7 @@ class Notifications {
16
16
  *
17
17
  * @param {Object} options
18
18
  * @param {Object} options.settingsCache - settings cache instance
19
- * @param {Object} options.ghostVersion
20
- * @param {String} options.ghostVersion.full - Ghost instance version in "full" format - major.minor.patch
19
+ * @param {String} options.ghostVersion - Ghost instance version in "full" format - major.minor.patch
21
20
  * @param {Object} options.SettingsModel - Ghost's Setting model instance
22
21
  */
23
22
  constructor({settingsCache, ghostVersion, SettingsModel}) {
@@ -75,40 +74,45 @@ class Notifications {
75
74
  browse({user}) {
76
75
  let allNotifications = this.fetchAllNotifications();
77
76
  allNotifications = _.orderBy(allNotifications, 'addedAt', 'desc');
77
+ const blogVersion = this.ghostVersion.match(/^(\d+\.)(\d+\.)(\d+)/);
78
78
 
79
79
  allNotifications = allNotifications.filter((notification) => {
80
- // NOTE: Filtering by version below is just a patch for bigger problem - notifications are not removed
81
- // after Ghost update. Logic below should be removed when Ghost upgrade detection
82
- // is done (https://github.com/TryGhost/Ghost/issues/10236) and notifications are
83
- // be removed permanently on upgrade event.
84
- const ghostMajorRegEx = /Ghost (?<major>\d).0 is now available/gi;
85
- const ghostSec43 = /GHSA-9fgx-q25h-jxrg/gi;
86
-
87
- // CASE: do not return old release notification
88
- if (notification.message
89
- && (!notification.custom || notification.message.match(ghostMajorRegEx) || notification.message.match(ghostSec43))) {
90
- let notificationVersion = notification.message.match(/(\d+\.)(\d+\.)(\d+)/);
91
-
92
- if (!notificationVersion && notification.message.match(ghostSec43)) {
93
- // Treating "GHSA-9fgx-q25h-jxrg" notification as 4.3.3 because there's no way to detect version
94
- // from it's message. In the future we should consider having a separate field with version
95
- // coming with each notification
96
- notificationVersion = ['4.3.3'];
97
- }
98
-
99
- const ghostMajorMatch = ghostMajorRegEx.exec(notification.message);
100
- if (ghostMajorMatch && ghostMajorMatch.groups && ghostMajorMatch.groups.major) {
101
- notificationVersion = `${ghostMajorMatch.groups.major}.0.0`;
102
- } else if (notificationVersion){
103
- notificationVersion = notificationVersion[0];
104
- }
105
-
106
- const blogVersion = this.ghostVersion.full.match(/^(\d+\.)(\d+\.)(\d+)/);
107
-
108
- if (notificationVersion && blogVersion && semver.gt(notificationVersion, blogVersion[0])) {
109
- return true;
110
- } else {
111
- return false;
80
+ if (notification.createdAtVersion && !this.wasSeen(notification, user)) {
81
+ return semver.gte(notification.createdAtVersion, blogVersion[0]);
82
+ } else {
83
+ // NOTE: Filtering by version below is just a patch for bigger problem - notifications are not removed
84
+ // after Ghost update. Logic below should be removed when Ghost upgrade detection
85
+ // is done (https://github.com/TryGhost/Ghost/issues/10236) and notifications are
86
+ // be removed permanently on upgrade event.
87
+ // NOTE: this whole else block can be removed with the first version after Ghost v5.0
88
+ // as the "createdAtVersion" mechanism will be enough to detect major version updates.
89
+ const ghostMajorRegEx = /Ghost (?<major>\d).0 is now available/gi;
90
+ const ghostSec43 = /GHSA-9fgx-q25h-jxrg/gi;
91
+
92
+ // CASE: do not return old release notification
93
+ if (notification.message
94
+ && (!notification.custom || notification.message.match(ghostMajorRegEx) || notification.message.match(ghostSec43))) {
95
+ let notificationVersion = notification.message.match(/(\d+\.)(\d+\.)(\d+)/);
96
+
97
+ if (!notificationVersion && notification.message.match(ghostSec43)) {
98
+ // Treating "GHSA-9fgx-q25h-jxrg" notification as 4.3.3 because there's no way to detect version
99
+ // from it's message. In the future we should consider having a separate field with version
100
+ // coming with each notification
101
+ notificationVersion = ['4.3.3'];
102
+ }
103
+
104
+ const ghostMajorMatch = ghostMajorRegEx.exec(notification.message);
105
+ if (ghostMajorMatch && ghostMajorMatch.groups && ghostMajorMatch.groups.major) {
106
+ notificationVersion = `${ghostMajorMatch.groups.major}.0.0`;
107
+ } else if (notificationVersion){
108
+ notificationVersion = notificationVersion[0];
109
+ }
110
+
111
+ if (notificationVersion && blogVersion && semver.gt(notificationVersion, blogVersion[0])) {
112
+ return true;
113
+ } else {
114
+ return false;
115
+ }
112
116
  }
113
117
  }
114
118
 
@@ -123,7 +127,8 @@ class Notifications {
123
127
  dismissible: true,
124
128
  location: 'bottom',
125
129
  status: 'alert',
126
- id: ObjectId().toHexString()
130
+ id: ObjectId().toHexString(),
131
+ createdAtVersion: this.ghostVersion
127
132
  };
128
133
 
129
134
  const overrides = {
@@ -1,5 +1,6 @@
1
1
  const Promise = require('bluebird');
2
2
  const errors = require('@tryghost/errors');
3
+ const tpl = require('@tryghost/tpl');
3
4
  const {extract, hasProvider} = require('oembed-parser');
4
5
  const cheerio = require('cheerio');
5
6
  const _ = require('lodash');
@@ -34,10 +35,6 @@ const findUrlWithProvider = (url) => {
34
35
  return {url, provider};
35
36
  };
36
37
 
37
- /**
38
- * @typedef {(string: string) => string} ITpl
39
- */
40
-
41
38
  /**
42
39
  * @typedef {Object} IConfig
43
40
  * @prop {(key: string) => string} get
@@ -51,19 +48,17 @@ class OEmbed {
51
48
  /**
52
49
  *
53
50
  * @param {Object} dependencies
54
- * @param {ITpl} dependencies.tpl
55
51
  * @param {IConfig} dependencies.config
56
52
  * @param {IExternalRequest} dependencies.externalRequest
57
53
  */
58
- constructor({config, externalRequest, tpl}) {
54
+ constructor({config, externalRequest}) {
59
55
  this.config = config;
60
56
  this.externalRequest = externalRequest;
61
- this.tpl = tpl;
62
57
  }
63
58
 
64
59
  unknownProvider(url) {
65
60
  return Promise.reject(new errors.ValidationError({
66
- message: this.tpl(messages.unknownProvider),
61
+ message: tpl(messages.unknownProvider),
67
62
  context: url
68
63
  }));
69
64
  }
@@ -133,7 +128,7 @@ class OEmbed {
133
128
  }
134
129
 
135
130
  return Promise.reject(new errors.ValidationError({
136
- message: this.tpl(messages.insufficientMetadata),
131
+ message: tpl(messages.insufficientMetadata),
137
132
  context: url
138
133
  }));
139
134
  }
@@ -11,33 +11,43 @@ const urlUtils = require('../../../shared/url-utils');
11
11
  const models = require('../../models');
12
12
 
13
13
  const redirectManager = new DynamicRedirectManager({
14
- permanentMaxAge: config.get('caching:customRedirects:maxAge')
15
- }, urlUtils);
14
+ permanentMaxAge: config.get('caching:customRedirects:maxAge'),
15
+ getSubdirectoryURL: (pathname) => {
16
+ return urlUtils.urlJoin(urlUtils.getSubdir(), pathname);
17
+ }
18
+ });
16
19
 
17
20
  module.exports = {
18
21
  async init() {
19
22
  const offersModule = OffersModule.create({
20
23
  OfferModel: models.Offer,
24
+ OfferRedemptionModel: models.OfferRedemption,
21
25
  redirectManager: redirectManager,
22
26
  stripeAPIService: stripeService.api
23
27
  });
24
28
 
25
29
  this.api = offersModule.api;
26
- this.repository = offersModule.repository;
27
30
 
31
+ let initCalled = false;
28
32
  if (labs.isSet('offers')) {
29
33
  // handles setting up redirects
30
- await offersModule.init();
34
+ const promise = offersModule.init();
35
+ initCalled = true;
36
+ await promise;
31
37
  }
32
38
 
33
39
  // TODO: Delete after GA
34
40
  let offersEnabled = labs.isSet('offers');
35
41
  events.on('settings.labs.edited', async () => {
36
- if (labs.isSet('offers') !== offersEnabled) {
42
+ if (labs.isSet('offers') && !initCalled) {
43
+ const promise = offersModule.init();
44
+ initCalled = true;
45
+ await promise;
46
+ } else if (labs.isSet('offers') !== offersEnabled) {
37
47
  offersEnabled = labs.isSet('offers');
38
48
 
39
49
  if (offersEnabled) {
40
- const offers = await this.api.listOffers();
50
+ const offers = await this.api.listOffers({});
41
51
  for (const offer of offers) {
42
52
  redirectManager.addRedirect(`/${offer.code}`, `/#/portal/offers/${offer.id}`, {permanent: false});
43
53
  }
@@ -1,10 +1,14 @@
1
1
  const _ = require('lodash');
2
2
  const Promise = require('bluebird');
3
3
  const errors = require('@tryghost/errors');
4
- const i18n = require('../../../shared/i18n');
4
+ const tpl = require('@tryghost/tpl');
5
5
  const parseContext = require('./parse-context');
6
6
  const _private = {};
7
7
 
8
+ const messages = {
9
+ error: 'You do not have permission to retrieve {docName} with that status'
10
+ };
11
+
8
12
  /**
9
13
  * @TODO:
10
14
  *
@@ -17,7 +21,7 @@ const _private = {};
17
21
  * - public context cannot fetch draft/scheduled posts
18
22
  */
19
23
  _private.applyStatusRules = function applyStatusRules(docName, method, opts) {
20
- const err = new errors.NoPermissionError({message: i18n.t('errors.permissions.applyStatusRules.error', {docName: docName})});
24
+ const err = new errors.NoPermissionError({message: tpl(messages.error, {docName: docName})});
21
25
 
22
26
  // Enforce status 'active' for users
23
27
  if (docName === 'users') {
@@ -3,7 +3,7 @@ const moment = require('moment-timezone');
3
3
  const fs = require('fs-extra');
4
4
  const path = require('path');
5
5
  const crypto = require('crypto');
6
- const urlService = require('../../../frontend/services/url');
6
+ const urlService = require('../url');
7
7
 
8
8
  const debug = require('@tryghost/debug')('services:route-settings');
9
9
  const errors = require('@tryghost/errors');
@@ -4,6 +4,7 @@
4
4
  */
5
5
  const events = require('../../lib/common/events');
6
6
  const models = require('../../models');
7
+ const labs = require('../../../shared/labs');
7
8
  const SettingsCache = require('../../../shared/settings-cache');
8
9
  const SettingsBREADService = require('./settings-bread-service');
9
10
  const {obfuscatedSetting, isSecretSetting, hideValueIfSecret} = require('./settings-utils');
@@ -14,7 +15,8 @@ const {obfuscatedSetting, isSecretSetting, hideValueIfSecret} = require('./setti
14
15
  const getSettingsBREADServiceInstance = () => {
15
16
  return new SettingsBREADService({
16
17
  SettingsModel: models.Settings,
17
- settingsCache: SettingsCache
18
+ settingsCache: SettingsCache,
19
+ labsService: labs
18
20
  });
19
21
  };
20
22
 
@@ -14,10 +14,12 @@ class SettingsBREADService {
14
14
  * @param {Object} options
15
15
  * @param {Object} options.SettingsModel
16
16
  * @param {Object} options.settingsCache - SettingsCache instance
17
+ * @param {Object} options.labsService - labs service instance
17
18
  */
18
- constructor({SettingsModel, settingsCache}) {
19
+ constructor({SettingsModel, settingsCache, labsService}) {
19
20
  this.SettingsModel = SettingsModel;
20
21
  this.settingsCache = settingsCache;
22
+ this.labs = labsService;
21
23
  }
22
24
 
23
25
  /**
@@ -28,24 +30,7 @@ class SettingsBREADService {
28
30
  browse(context) {
29
31
  let settings = this.settingsCache.getAll();
30
32
 
31
- // CASE: no context passed (functional call)
32
- if (!context) {
33
- return Promise.resolve(settings.filter((setting) => {
34
- return setting.group === 'site';
35
- }));
36
- }
37
-
38
- if (!context.internal) {
39
- // CASE: omit core settings unless internal request
40
- settings = _.filter(settings, (setting) => {
41
- const isCore = setting.group === 'core';
42
- return !isCore;
43
- });
44
- // CASE: omit secret settings unless internal request
45
- settings = settings.map(hideValueIfSecret);
46
- }
47
-
48
- return settings;
33
+ return this._formatBrowse(settings, context);
49
34
  }
50
35
 
51
36
  /**
@@ -86,6 +71,12 @@ class SettingsBREADService {
86
71
  }));
87
72
  }
88
73
 
74
+ // NOTE: Labs flags can exist outside of the DB when they are forced on/off
75
+ // so we grab them from the labs service instead as that's source-of-truth
76
+ if (setting.key === 'labs') {
77
+ setting.value = JSON.stringify(this.labs.getAll());
78
+ }
79
+
89
80
  setting = hideValueIfSecret(setting);
90
81
 
91
82
  return {
@@ -161,7 +152,9 @@ class SettingsBREADService {
161
152
  });
162
153
  }
163
154
 
164
- return this.SettingsModel.edit(filteredSettings, options);
155
+ return this.SettingsModel.edit(filteredSettings, options).then((result) => {
156
+ return this._formatBrowse(_.keyBy(_.invokeMap(result, 'toJSON'), 'key'), options.context);
157
+ });
165
158
  }
166
159
 
167
160
  /**
@@ -183,6 +176,35 @@ class SettingsBREADService {
183
176
  }
184
177
  }
185
178
  }
179
+
180
+ _formatBrowse(inputSettings, context) {
181
+ let settings = _.values(inputSettings);
182
+ // CASE: no context passed (functional call)
183
+ if (!context) {
184
+ return Promise.resolve(settings.filter((setting) => {
185
+ return setting.group === 'site';
186
+ }));
187
+ }
188
+
189
+ if (!context.internal) {
190
+ // CASE: omit core settings unless internal request
191
+ settings = _.filter(settings, (setting) => {
192
+ const isCore = setting.group === 'core';
193
+ return !isCore;
194
+ });
195
+ // CASE: omit secret settings unless internal request
196
+ settings = settings.map(hideValueIfSecret);
197
+ }
198
+
199
+ // NOTE: Labs flags can exist outside of the DB when they are forced on/off
200
+ // so we grab them from the labs service instead as that's source-of-truth
201
+ const labsSetting = settings.find(setting => setting.key === 'labs');
202
+ if (labsSetting) {
203
+ labsSetting.value = JSON.stringify(this.labs.getAll());
204
+ }
205
+
206
+ return settings;
207
+ }
186
208
  }
187
209
 
188
210
  module.exports = SettingsBREADService;
@@ -4,7 +4,7 @@ const logging = require('@tryghost/logging');
4
4
  const request = require('@tryghost/request');
5
5
  const {blogIcon} = require('../lib/image');
6
6
  const urlUtils = require('../../shared/url-utils');
7
- const urlService = require('../../frontend/services/url');
7
+ const urlService = require('./url');
8
8
  const settingsCache = require('../../shared/settings-cache');
9
9
  const schema = require('../data/schema').checks;
10
10
  const moment = require('moment');
@@ -30,7 +30,7 @@ module.exports.loadAndActivate = async (themeName) => {
30
30
  logging.warn(validate.getThemeValidationError('activeThemeHasErrors', themeName, checkedTheme));
31
31
  }
32
32
 
33
- activator.activateFromBoot(themeName, loadedTheme, checkedTheme);
33
+ await activator.activateFromBoot(themeName, loadedTheme, checkedTheme);
34
34
  } catch (err) {
35
35
  if (err instanceof errors.NotFoundError) {
36
36
  // CASE: active theme is missing, we don't want to exit because the admin panel will still work
@@ -56,7 +56,7 @@ module.exports.activate = async (themeName) => {
56
56
  // Validate
57
57
  const checkedTheme = await validate.checkSafe(themeName, loadedTheme);
58
58
  // Activate
59
- activator.activateFromAPI(themeName, loadedTheme, checkedTheme);
59
+ await activator.activateFromAPI(themeName, loadedTheme, checkedTheme);
60
60
  // Return the checked theme
61
61
  return checkedTheme;
62
62
  };
@@ -8,27 +8,27 @@ const customThemeSettings = require('../custom-theme-settings');
8
8
  * And also adds a little debug statement, which is very handy when debugging theme logic
9
9
  */
10
10
  module.exports = {
11
- activateFromBoot: (themeName, theme, checkedTheme) => {
11
+ activateFromBoot: async (themeName, theme, checkedTheme) => {
12
12
  debug('Activating theme (method A on boot)', themeName);
13
13
  // TODO: probably a better place for this to happen - after successful activation / when reloading site?
14
14
  if (labs.isSet('customThemeSettings')) {
15
- customThemeSettings.activateTheme(checkedTheme);
15
+ await customThemeSettings.api.activateTheme(themeName, checkedTheme);
16
16
  }
17
17
  bridge.activateTheme(theme, checkedTheme);
18
18
  },
19
- activateFromAPI: (themeName, theme, checkedTheme) => {
19
+ activateFromAPI: async (themeName, theme, checkedTheme) => {
20
20
  debug('Activating theme (method B on API "activate")', themeName);
21
21
  // TODO: probably a better place for this to happen - after successful activation / when reloading site?
22
22
  if (labs.isSet('customThemeSettings')) {
23
- customThemeSettings.activateTheme(checkedTheme);
23
+ await customThemeSettings.api.activateTheme(themeName, checkedTheme);
24
24
  }
25
25
  bridge.activateTheme(theme, checkedTheme);
26
26
  },
27
- activateFromAPIOverride: (themeName, theme, checkedTheme) => {
27
+ activateFromAPIOverride: async (themeName, theme, checkedTheme) => {
28
28
  debug('Activating theme (method C on API "override")', themeName);
29
29
  // TODO: probably a better place for this to happen - after successful activation / when reloading site?
30
30
  if (labs.isSet('customThemeSettings')) {
31
- customThemeSettings.activateTheme(checkedTheme);
31
+ await customThemeSettings.api.activateTheme(themeName, checkedTheme);
32
32
  }
33
33
  bridge.activateTheme(theme, checkedTheme);
34
34
  }
@@ -83,7 +83,7 @@ module.exports = {
83
83
  // CASE: if this is the active theme, we are overriding
84
84
  if (overrideTheme) {
85
85
  debug('setFromZip Theme is active already');
86
- activator.activateFromAPIOverride(themeName, loadedTheme, checkedTheme);
86
+ await activator.activateFromAPIOverride(themeName, loadedTheme, checkedTheme);
87
87
  }
88
88
 
89
89
  // @TODO: unify the name across gscan and Ghost!
File without changes
@@ -3,10 +3,10 @@ const Promise = require('bluebird');
3
3
  const debug = require('@tryghost/debug')('services:url:resources');
4
4
  const Resource = require('./Resource');
5
5
  const config = require('../../../shared/config');
6
- const models = require('../../../server/models');
6
+ const models = require('../../models');
7
7
 
8
8
  // This listens to all manner of model events to find new content that needs a URL...
9
- const events = require('../../../server/lib/common/events');
9
+ const events = require('../../lib/common/events');
10
10
 
11
11
  /**
12
12
  * @description At the moment the resources class is directly responsible for data population
@@ -68,24 +68,24 @@ class UrlGenerator {
68
68
  }
69
69
 
70
70
  /**
71
- * @description Helper function to register listeners for each url generator instance.
72
- * @private
71
+ * @NOTE: currently only used if the permalink setting changes and it's used for this url generator.
72
+ * @TODO: https://github.com/TryGhost/Ghost/issues/10699
73
73
  */
74
- _listeners() {
75
- /**
76
- * @NOTE: currently only used if the permalink setting changes and it's used for this url generator.
77
- * @TODO: https://github.com/TryGhost/Ghost/issues/10699
78
- */
79
- this.router.addListener('updated', () => {
80
- const myResources = this.urls.getByGeneratorId(this.uid);
74
+ regenerateResources() {
75
+ const myResources = this.urls.getByGeneratorId(this.uid);
81
76
 
82
- myResources.forEach((object) => {
83
- this.urls.removeResourceId(object.resource.data.id);
84
- object.resource.release();
85
- this._try(object.resource);
86
- });
77
+ myResources.forEach((object) => {
78
+ this.urls.removeResourceId(object.resource.data.id);
79
+ object.resource.release();
80
+ this._try(object.resource);
87
81
  });
82
+ }
88
83
 
84
+ /**
85
+ * @description Helper function to register listeners for each url generator instance.
86
+ * @private
87
+ */
88
+ _listeners() {
89
89
  /**
90
90
  * Listen on two events:
91
91
  *