ghost 5.25.4 → 5.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/components/tryghost-adapter-manager-5.26.0.tgz +0 -0
  2. package/components/{tryghost-api-framework-5.25.4.tgz → tryghost-api-framework-5.26.0.tgz} +0 -0
  3. package/components/{tryghost-api-version-compatibility-service-5.25.4.tgz → tryghost-api-version-compatibility-service-5.26.0.tgz} +0 -0
  4. package/components/tryghost-audience-feedback-5.26.0.tgz +0 -0
  5. package/components/tryghost-bootstrap-socket-5.26.0.tgz +0 -0
  6. package/components/tryghost-constants-5.26.0.tgz +0 -0
  7. package/components/{tryghost-custom-theme-settings-service-5.25.4.tgz → tryghost-custom-theme-settings-service-5.26.0.tgz} +0 -0
  8. package/components/{tryghost-data-generator-5.25.4.tgz → tryghost-data-generator-5.26.0.tgz} +0 -0
  9. package/components/tryghost-domain-events-5.26.0.tgz +0 -0
  10. package/components/tryghost-email-analytics-provider-mailgun-5.26.0.tgz +0 -0
  11. package/components/tryghost-email-analytics-service-5.26.0.tgz +0 -0
  12. package/components/tryghost-email-content-generator-5.26.0.tgz +0 -0
  13. package/components/tryghost-email-events-5.26.0.tgz +0 -0
  14. package/components/tryghost-email-service-5.26.0.tgz +0 -0
  15. package/components/tryghost-email-suppression-list-5.26.0.tgz +0 -0
  16. package/components/tryghost-express-dynamic-redirects-5.26.0.tgz +0 -0
  17. package/components/tryghost-extract-api-key-5.26.0.tgz +0 -0
  18. package/components/tryghost-html-to-plaintext-5.26.0.tgz +0 -0
  19. package/components/tryghost-importer-revue-5.26.0.tgz +0 -0
  20. package/components/tryghost-job-manager-5.26.0.tgz +0 -0
  21. package/components/tryghost-link-redirects-5.26.0.tgz +0 -0
  22. package/components/tryghost-link-replacer-5.26.0.tgz +0 -0
  23. package/components/tryghost-link-tracking-5.26.0.tgz +0 -0
  24. package/components/tryghost-magic-link-5.26.0.tgz +0 -0
  25. package/components/tryghost-mailgun-client-5.26.0.tgz +0 -0
  26. package/components/tryghost-member-attribution-5.26.0.tgz +0 -0
  27. package/components/{tryghost-member-events-5.25.4.tgz → tryghost-member-events-5.26.0.tgz} +0 -0
  28. package/components/tryghost-members-api-5.26.0.tgz +0 -0
  29. package/components/tryghost-members-csv-5.26.0.tgz +0 -0
  30. package/components/tryghost-members-events-service-5.26.0.tgz +0 -0
  31. package/components/{tryghost-members-importer-5.25.4.tgz → tryghost-members-importer-5.26.0.tgz} +0 -0
  32. package/components/tryghost-members-offers-5.26.0.tgz +0 -0
  33. package/components/tryghost-members-payments-5.26.0.tgz +0 -0
  34. package/components/tryghost-members-ssr-5.26.0.tgz +0 -0
  35. package/components/{tryghost-members-stripe-service-5.25.4.tgz → tryghost-members-stripe-service-5.26.0.tgz} +0 -0
  36. package/components/tryghost-minifier-5.26.0.tgz +0 -0
  37. package/components/tryghost-mw-api-version-mismatch-5.26.0.tgz +0 -0
  38. package/components/tryghost-mw-cache-control-5.26.0.tgz +0 -0
  39. package/components/tryghost-mw-error-handler-5.26.0.tgz +0 -0
  40. package/components/tryghost-mw-session-from-token-5.26.0.tgz +0 -0
  41. package/components/tryghost-mw-update-user-last-seen-5.26.0.tgz +0 -0
  42. package/components/tryghost-mw-vhost-5.26.0.tgz +0 -0
  43. package/components/tryghost-oembed-service-5.26.0.tgz +0 -0
  44. package/components/tryghost-package-json-5.26.0.tgz +0 -0
  45. package/components/tryghost-referrers-5.26.0.tgz +0 -0
  46. package/components/tryghost-security-5.26.0.tgz +0 -0
  47. package/components/tryghost-session-service-5.26.0.tgz +0 -0
  48. package/components/tryghost-settings-path-manager-5.26.0.tgz +0 -0
  49. package/components/{tryghost-staff-service-5.25.4.tgz → tryghost-staff-service-5.26.0.tgz} +0 -0
  50. package/components/tryghost-stats-service-5.26.0.tgz +0 -0
  51. package/components/tryghost-tiers-5.26.0.tgz +0 -0
  52. package/components/tryghost-update-check-service-5.26.0.tgz +0 -0
  53. package/components/tryghost-verification-trigger-5.26.0.tgz +0 -0
  54. package/components/tryghost-version-notifications-data-service-5.26.0.tgz +0 -0
  55. package/content/themes/casper/assets/built/casper.js +1 -1
  56. package/content/themes/casper/assets/built/casper.js.map +1 -1
  57. package/content/themes/casper/assets/built/screen.css +1 -1
  58. package/content/themes/casper/assets/built/screen.css.map +1 -1
  59. package/content/themes/casper/assets/css/screen.css +88 -17
  60. package/content/themes/casper/assets/js/dropdown.js +4 -3
  61. package/content/themes/casper/assets/js/infinite-scroll.js +2 -0
  62. package/content/themes/casper/author.hbs +57 -51
  63. package/content/themes/casper/default.hbs +12 -9
  64. package/content/themes/casper/index.hbs +2 -0
  65. package/content/themes/casper/package.json +1 -1
  66. package/content/themes/casper/tag.hbs +3 -0
  67. package/core/built/admin/assets/{chunk.143.c62201923eb26b9e05e6.js → chunk.143.51c2ca78fc65765021bd.js} +20 -20
  68. package/core/built/admin/assets/{chunk.178.0a0a56a35334fe81feb9.js → chunk.178.ef1e477ec5665597a79a.js} +4 -4
  69. package/core/built/admin/assets/{chunk.507.f7ed7e0b5ac069083e0c.js → chunk.507.18a4d11e74a0b6e03a69.js} +107 -102
  70. package/core/built/admin/assets/{chunk.613.551c7c3e872b9a811863.js → chunk.613.25718fa355a25fc2a7c1.js} +163 -152
  71. package/core/built/admin/assets/{chunk.613.551c7c3e872b9a811863.js.LICENSE.txt → chunk.613.25718fa355a25fc2a7c1.js.LICENSE.txt} +0 -0
  72. package/core/built/admin/assets/{ghost-15f77300dbb6762873d6692e0485a37a.js → ghost-5429d972966c85a0a24d766f93db999d.js} +3856 -2903
  73. package/core/built/admin/assets/{ghost-86cf21b24691d4fe9a73bcf84e5b36fe.css → ghost-830670d910cea64d98ab083aafd1dffd.css} +1 -1
  74. package/core/built/admin/assets/{ghost-dark-2e0bf27a167574e0b2ca9dce1c9ec188.css → ghost-dark-e9195020925a360d83163e1e87b514a6.css} +1 -1
  75. package/core/built/admin/assets/img/themes/Alto-0dfe76694ed222d6d96fc9c8db979a38.png +0 -0
  76. package/core/built/admin/assets/img/themes/Bulletin-d66dec818ad0ba2965dd7eb3130c621c.png +0 -0
  77. package/core/built/admin/assets/img/themes/Casper-9a0ce71df3a1c589c1414ad2aa5b8aeb.png +0 -0
  78. package/core/built/admin/assets/img/themes/Dawn-302fbfdbc352098a256137159bd83dd8.png +0 -0
  79. package/core/built/admin/assets/img/themes/Digest-698e78d8e8481daff8ae4a8647528dc9.png +0 -0
  80. package/core/built/admin/assets/img/themes/Dope-d099dfca697adae16baa76f89520c5b3.png +0 -0
  81. package/core/built/admin/assets/img/themes/Ease-7075f809892f10c58892e15a57a4aae4.png +0 -0
  82. package/core/built/admin/assets/img/themes/Edge-1e5e0eec6941d7bdca02cebb66187357.png +0 -0
  83. package/core/built/admin/assets/img/themes/Edition-10111a2b8458168dcff81b7fb151be70.png +0 -0
  84. package/core/built/admin/assets/img/themes/Episode-e4c86d1f75ef1d8a77791d7fd519fdd4.png +0 -0
  85. package/core/built/admin/assets/img/themes/Headline-f70eaf49b9fcae1ddfe3d4496d8be54d.png +0 -0
  86. package/core/built/admin/assets/img/themes/Journal-07d35b2311501d2738bad1907ba2f7e1.png +0 -0
  87. package/core/built/admin/assets/img/themes/London-4e042390da16fecef947f3a7001d03db.png +0 -0
  88. package/core/built/admin/assets/img/themes/Ruby-b896885448e0f28ca62c6dcd56c732aa.png +0 -0
  89. package/core/built/admin/assets/img/themes/Solo-0292eb9ae0ca7b578cff50824d40cc86.png +0 -0
  90. package/core/built/admin/assets/img/themes/Taste-a24d2a786900d9caff7e773ca0040991.png +0 -0
  91. package/core/built/admin/assets/img/themes/Wave-b98fadfd3ed16e8b2e383dd8e8e5dca2.png +0 -0
  92. package/core/built/admin/assets/{vendor-0d29a566ca9747f2cfba9d7c98e39f53.js → vendor-5aae14724f891e36c43786254980485c.js} +1451 -1161
  93. package/core/built/admin/index.html +6 -6
  94. package/core/frontend/helpers/get.js +67 -16
  95. package/core/server/api/endpoints/db.js +1 -1
  96. package/core/server/api/endpoints/utils/serializers/output/mappers/newsletters.js +1 -0
  97. package/core/server/data/importer/handlers/json.js +2 -0
  98. package/core/server/data/importer/handlers/revue.js +44 -0
  99. package/core/server/data/importer/import-manager.js +22 -10
  100. package/core/server/data/importer/importers/data/data-importer.js +5 -1
  101. package/core/server/data/importer/importers/data/posts.js +4 -0
  102. package/core/server/data/importer/importers/data/revue-subscriber.js +55 -0
  103. package/core/server/services/mega/mega.js +10 -4
  104. package/core/server/services/members/middleware.js +18 -3
  105. package/core/server/services/members-events/index.js +4 -1
  106. package/core/server/services/stripe/service.js +3 -1
  107. package/core/shared/config/defaults.json +14 -6
  108. package/package.json +140 -137
  109. package/yarn.lock +586 -530
  110. package/components/tryghost-adapter-manager-5.25.4.tgz +0 -0
  111. package/components/tryghost-audience-feedback-5.25.4.tgz +0 -0
  112. package/components/tryghost-bootstrap-socket-5.25.4.tgz +0 -0
  113. package/components/tryghost-constants-5.25.4.tgz +0 -0
  114. package/components/tryghost-domain-events-5.25.4.tgz +0 -0
  115. package/components/tryghost-email-analytics-provider-mailgun-5.25.4.tgz +0 -0
  116. package/components/tryghost-email-analytics-service-5.25.4.tgz +0 -0
  117. package/components/tryghost-email-content-generator-5.25.4.tgz +0 -0
  118. package/components/tryghost-email-events-5.25.4.tgz +0 -0
  119. package/components/tryghost-email-service-5.25.4.tgz +0 -0
  120. package/components/tryghost-email-suppression-list-5.25.4.tgz +0 -0
  121. package/components/tryghost-express-dynamic-redirects-5.25.4.tgz +0 -0
  122. package/components/tryghost-extract-api-key-5.25.4.tgz +0 -0
  123. package/components/tryghost-html-to-plaintext-5.25.4.tgz +0 -0
  124. package/components/tryghost-job-manager-5.25.4.tgz +0 -0
  125. package/components/tryghost-link-redirects-5.25.4.tgz +0 -0
  126. package/components/tryghost-link-replacer-5.25.4.tgz +0 -0
  127. package/components/tryghost-link-tracking-5.25.4.tgz +0 -0
  128. package/components/tryghost-magic-link-5.25.4.tgz +0 -0
  129. package/components/tryghost-mailgun-client-5.25.4.tgz +0 -0
  130. package/components/tryghost-member-attribution-5.25.4.tgz +0 -0
  131. package/components/tryghost-members-api-5.25.4.tgz +0 -0
  132. package/components/tryghost-members-csv-5.25.4.tgz +0 -0
  133. package/components/tryghost-members-events-service-5.25.4.tgz +0 -0
  134. package/components/tryghost-members-offers-5.25.4.tgz +0 -0
  135. package/components/tryghost-members-payments-5.25.4.tgz +0 -0
  136. package/components/tryghost-members-ssr-5.25.4.tgz +0 -0
  137. package/components/tryghost-minifier-5.25.4.tgz +0 -0
  138. package/components/tryghost-mw-api-version-mismatch-5.25.4.tgz +0 -0
  139. package/components/tryghost-mw-cache-control-5.25.4.tgz +0 -0
  140. package/components/tryghost-mw-error-handler-5.25.4.tgz +0 -0
  141. package/components/tryghost-mw-session-from-token-5.25.4.tgz +0 -0
  142. package/components/tryghost-mw-update-user-last-seen-5.25.4.tgz +0 -0
  143. package/components/tryghost-mw-vhost-5.25.4.tgz +0 -0
  144. package/components/tryghost-oembed-service-5.25.4.tgz +0 -0
  145. package/components/tryghost-package-json-5.25.4.tgz +0 -0
  146. package/components/tryghost-referrers-5.25.4.tgz +0 -0
  147. package/components/tryghost-security-5.25.4.tgz +0 -0
  148. package/components/tryghost-session-service-5.25.4.tgz +0 -0
  149. package/components/tryghost-settings-path-manager-5.25.4.tgz +0 -0
  150. package/components/tryghost-stats-service-5.25.4.tgz +0 -0
  151. package/components/tryghost-tiers-5.25.4.tgz +0 -0
  152. package/components/tryghost-update-check-service-5.25.4.tgz +0 -0
  153. package/components/tryghost-verification-trigger-5.25.4.tgz +0 -0
  154. package/components/tryghost-version-notifications-data-service-5.25.4.tgz +0 -0
  155. package/core/built/admin/assets/img/themes/Alto-f4db5af43ca9771c7ac1f754de3ddf2f.png +0 -0
  156. package/core/built/admin/assets/img/themes/Bulletin-57d45b992ff0e26e0acdce7ed4cccd67.png +0 -0
  157. package/core/built/admin/assets/img/themes/Casper-19b7267aac5acc6abfaaed7e41eddae8.jpg +0 -0
  158. package/core/built/admin/assets/img/themes/Dawn-be81aa8c8caae8fcfb5d5fbec823fdcc.png +0 -0
  159. package/core/built/admin/assets/img/themes/Digest-d3467ac22a290e1ad3a543014758286e.png +0 -0
  160. package/core/built/admin/assets/img/themes/Dope-6f8e0bbc199ce4af9a60859e9e6a74ad.png +0 -0
  161. package/core/built/admin/assets/img/themes/Ease-9c279ea6cec3c0f1823f81c9dd24b116.png +0 -0
  162. package/core/built/admin/assets/img/themes/Edge-0258906309e11fd075a1d9880aa09b20.png +0 -0
  163. package/core/built/admin/assets/img/themes/Edition-d8f508e93bc24bdf2716ae6f8b3d44f8.png +0 -0
  164. package/core/built/admin/assets/img/themes/Headline-c5070cf549e797a6a72b87237caa1617.jpg +0 -0
  165. package/core/built/admin/assets/img/themes/Journal-accf0031bbae0919900a049061e65a04.png +0 -0
  166. package/core/built/admin/assets/img/themes/London-3f07efcee9e5bfb9a33827064eb77e70.jpg +0 -0
  167. package/core/built/admin/assets/img/themes/Ruby-11a53c62015612f4b3aca8f503121225.png +0 -0
  168. package/core/built/admin/assets/img/themes/Solo-8634b5681d888995e0f5fe8ac1a27ba0.png +0 -0
  169. package/core/built/admin/assets/img/themes/Wave-86e8044c2d76cb57a9030e4c24ac9520.png +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%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.25%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%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%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.26%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%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-3e6947aa681f0fb82b193090e520dc73.css">
40
- <link integrity="" rel="stylesheet" href="assets/ghost-86cf21b24691d4fe9a73bcf84e5b36fe.css" title="light">
40
+ <link integrity="" rel="stylesheet" href="assets/ghost-830670d910cea64d98ab083aafd1dffd.css" title="light">
41
41
 
42
42
 
43
43
  </head>
@@ -56,9 +56,9 @@
56
56
 
57
57
  <div id="ember-basic-dropdown-wormhole"></div>
58
58
 
59
- <script src="assets/vendor-0d29a566ca9747f2cfba9d7c98e39f53.js"></script>
60
- <script src="assets/chunk.613.551c7c3e872b9a811863.js"></script>
61
- <script src="assets/chunk.143.c62201923eb26b9e05e6.js"></script>
62
- <script src="assets/ghost-15f77300dbb6762873d6692e0485a37a.js"></script>
59
+ <script src="assets/vendor-5aae14724f891e36c43786254980485c.js"></script>
60
+ <script src="assets/chunk.613.25718fa355a25fc2a7c1.js"></script>
61
+ <script src="assets/chunk.143.51c2ca78fc65765021bd.js"></script>
62
+ <script src="assets/ghost-5429d972966c85a0a24d766f93db999d.js"></script>
63
63
  </body>
64
64
  </html>
@@ -117,9 +117,59 @@ function parseOptions(globals, data, options) {
117
117
  return options;
118
118
  }
119
119
 
120
+ /**
121
+ *
122
+ * @param {String} resource
123
+ * @param {String} controllerName
124
+ * @param {String} action
125
+ * @param {Object} apiOptions
126
+ * @returns {Promise<Object>}
127
+ */
128
+ async function makeAPICall(resource, controllerName, action, apiOptions) {
129
+ const controller = api[controllerName];
130
+
131
+ let timer;
132
+
133
+ try {
134
+ let response;
135
+
136
+ if (config.get('optimization:getHelper:timeout:threshold')) {
137
+ const logLevel = config.get('optimization:getHelper:timeout:level') || 'error';
138
+ const threshold = config.get('optimization:getHelper:timeout:threshold');
139
+
140
+ const apiResponse = controller[action](apiOptions);
141
+
142
+ const timeout = new Promise((resolve) => {
143
+ timer = setTimeout(() => {
144
+ logging[logLevel](new errors.HelperWarning({
145
+ message: `{{#get}} took longer than ${threshold}ms and was aborted`,
146
+ code: 'ABORTED_GET_HELPER',
147
+ errorDetails: {
148
+ api: `${controllerName}.${action}`,
149
+ apiOptions
150
+ }
151
+ }));
152
+
153
+ resolve({[resource]: []});
154
+ }, threshold);
155
+ });
156
+
157
+ response = await Promise.race([apiResponse, timeout]);
158
+ clearTimeout(timer);
159
+ } else {
160
+ response = await controller[action](apiOptions);
161
+ }
162
+
163
+ return response;
164
+ } catch (err) {
165
+ clearTimeout(timer);
166
+ throw err;
167
+ }
168
+ }
169
+
120
170
  /**
121
171
  * ## Get
122
- * @param {Object} resource
172
+ * @param {String} resource
123
173
  * @param {Object} options
124
174
  * @returns {Promise<any>}
125
175
  */
@@ -149,7 +199,6 @@ module.exports = async function get(resource, options) {
149
199
  }
150
200
 
151
201
  const controllerName = RESOURCES[resource].alias;
152
- const controller = api[controllerName];
153
202
  const action = isBrowse(apiOptions) ? 'browse' : 'read';
154
203
 
155
204
  // Parse the options we're going to pass to the API
@@ -157,7 +206,7 @@ module.exports = async function get(resource, options) {
157
206
  apiOptions.context = {member: data.member};
158
207
 
159
208
  try {
160
- const response = await controller[action](apiOptions);
209
+ const response = await makeAPICall(resource, controllerName, action, apiOptions);
161
210
 
162
211
  // prepare data properties for use with handlebars
163
212
  if (response[resource] && response[resource].length) {
@@ -185,19 +234,21 @@ module.exports = async function get(resource, options) {
185
234
  data.error = error.message;
186
235
  return options.inverse(self, {data: data});
187
236
  } finally {
188
- const totalMs = Date.now() - start;
189
- const logLevel = config.get('logging:slowHelper:level');
190
- const threshold = config.get('logging:slowHelper:threshold');
191
- if (totalMs > threshold) {
192
- logging[logLevel](new errors.HelperWarning({
193
- message: `{{#get}} helper took ${totalMs}ms to complete`,
194
- code: 'SLOW_GET_HELPER',
195
- errorDetails: {
196
- api: `${controllerName}.${action}`,
197
- apiOptions,
198
- returnedRows: returnedRowsCount
199
- }
200
- }));
237
+ if (config.get('optimization:getHelper:notify:threshold')) {
238
+ const totalMs = Date.now() - start;
239
+ const logLevel = config.get('optimization:getHelper:notify:level') || 'warn';
240
+ const threshold = config.get('optimization:getHelper:notify:threshold');
241
+ if (totalMs > threshold) {
242
+ logging[logLevel](new errors.HelperWarning({
243
+ message: `{{#get}} helper took ${totalMs}ms to complete`,
244
+ code: 'SLOW_GET_HELPER',
245
+ errorDetails: {
246
+ api: `${controllerName}.${action}`,
247
+ apiOptions,
248
+ returnedRows: returnedRowsCount
249
+ }
250
+ }));
251
+ }
201
252
  }
202
253
  }
203
254
  };
@@ -83,7 +83,7 @@ module.exports = {
83
83
  permissions: true,
84
84
  query(frame) {
85
85
  const siteTimezone = settingsCache.get('timezone');
86
- const importTag = `Import ${moment().tz(siteTimezone).format('YYYY-MM-DD HH:mm')}`;
86
+ const importTag = `#Import ${moment().tz(siteTimezone).format('YYYY-MM-DD HH:mm')}`;
87
87
  return importer.importFromFile(frame.file, {
88
88
  user: {
89
89
  email: frame.user.get('email')
@@ -10,6 +10,7 @@ module.exports = (model, frame) => {
10
10
  name: jsonModel.name,
11
11
  description: jsonModel.description,
12
12
  slug: jsonModel.slug,
13
+ sender_email: jsonModel.sender_email,
13
14
  subscribe_on_signup: jsonModel.subscribe_on_signup,
14
15
  visibility: jsonModel.visibility,
15
16
  sort_order: jsonModel.sort_order,
@@ -2,6 +2,7 @@ const _ = require('lodash');
2
2
  const fs = require('fs-extra');
3
3
  const tpl = require('@tryghost/tpl');
4
4
  const errors = require('@tryghost/errors');
5
+ const debug = require('@tryghost/debug')('importer:handler:data');
5
6
 
6
7
  const messages = {
7
8
  invalidJsonFormat: 'Invalid JSON format, expected `{ db: [exportedData] }`',
@@ -17,6 +18,7 @@ JSONHandler = {
17
18
  directories: [],
18
19
 
19
20
  loadFile: async function (files, startDir) { // eslint-disable-line no-unused-vars
21
+ debug('loadFile', files);
20
22
  // @TODO: Handle multiple JSON files
21
23
  const filePath = files[0].path;
22
24
 
@@ -0,0 +1,44 @@
1
+ const _ = require('lodash');
2
+ const fs = require('fs-extra');
3
+ const debug = require('@tryghost/debug')('importer:handler:revue');
4
+
5
+ const hasIssuesCSV = (files) => {
6
+ return _.some(files, (file) => {
7
+ return file.name.match(/^issues.*?\.csv/);
8
+ });
9
+ };
10
+
11
+ const RevueHandler = {
12
+ type: 'revue',
13
+ extensions: ['.csv', '.json'],
14
+ contentTypes: ['application/octet-stream', 'application/json', 'text/plain'],
15
+ directories: [],
16
+
17
+ loadFile: function (files, startDir) {
18
+ debug('loadFile', files);
19
+ const startDirRegex = startDir ? new RegExp('^' + startDir + '/') : new RegExp('');
20
+ const idRegex = /_.*?\./;
21
+ const ops = [];
22
+ const revue = {};
23
+
24
+ if (!hasIssuesCSV(files)) {
25
+ return Promise.resolve();
26
+ }
27
+
28
+ _.each(files, function (file) {
29
+ ops.push(fs.readFile(file.path).then(function (content) {
30
+ // normalize the file name
31
+ file.name = file.name.replace(startDirRegex, '').replace(idRegex, '.');
32
+ const name = file.name.split('.')[0];
33
+
34
+ revue[name] = content.toString();
35
+ }));
36
+ });
37
+
38
+ return Promise.all(ops).then(() => {
39
+ return {meta: {revue: true}, revue};
40
+ });
41
+ }
42
+ };
43
+
44
+ module.exports = RevueHandler;
@@ -7,12 +7,15 @@ const uuid = require('uuid');
7
7
  const config = require('../../../shared/config');
8
8
  const {extract} = require('@tryghost/zip');
9
9
  const tpl = require('@tryghost/tpl');
10
+ const debug = require('@tryghost/debug')('import-manager');
10
11
  const logging = require('@tryghost/logging');
11
12
  const errors = require('@tryghost/errors');
12
13
  const ImageHandler = require('./handlers/image');
14
+ const RevueHandler = require('./handlers/revue');
13
15
  const JSONHandler = require('./handlers/json');
14
16
  const MarkdownHandler = require('./handlers/markdown');
15
17
  const ImageImporter = require('./importers/image');
18
+ const RevueImporter = require('@tryghost/importer-revue');
16
19
  const DataImporter = require('./importers/data');
17
20
  const urlUtils = require('../../../shared/url-utils');
18
21
  const {GhostMailer} = require('../../services/mail');
@@ -48,12 +51,12 @@ class ImportManager {
48
51
  /**
49
52
  * @type {Importer[]} importers
50
53
  */
51
- this.importers = [ImageImporter, DataImporter];
54
+ this.importers = [ImageImporter, RevueImporter, DataImporter];
52
55
 
53
56
  /**
54
57
  * @type {Handler[]}
55
58
  */
56
- this.handlers = [ImageHandler, JSONHandler, MarkdownHandler];
59
+ this.handlers = [ImageHandler, RevueHandler, JSONHandler, MarkdownHandler];
57
60
 
58
61
  // Keep track of file to cleanup at the end
59
62
  /**
@@ -240,6 +243,8 @@ class ImportManager {
240
243
  for (const handler of this.handlers) {
241
244
  const files = this.getFilesFromZip(handler, zipDirectory);
242
245
 
246
+ debug('handler', handler.type, files);
247
+
243
248
  if (files.length > 0) {
244
249
  if (Object.prototype.hasOwnProperty.call(importData, handler.type)) {
245
250
  // This limitation is here to reduce the complexity of the importer for now
@@ -271,17 +276,19 @@ class ImportManager {
271
276
  * @param {File} file
272
277
  * @returns {Promise<ImportData>}
273
278
  */
274
- processFile(file, ext) {
275
- const fileHandler = _.find(this.handlers, function (handler) {
279
+ async processFile(file, ext) {
280
+ const fileHandlers = _.filter(this.handlers, function (handler) {
276
281
  return _.includes(handler.extensions, ext);
277
282
  });
278
283
 
279
- return fileHandler.loadFile([_.pick(file, 'name', 'path')]).then(function (loadedData) {
280
- // normalize the returned data
281
- const importData = {};
282
- importData[fileHandler.type] = loadedData;
283
- return importData;
284
- });
284
+ const importData = {};
285
+
286
+ await Promise.all(fileHandlers.map(async (fileHandler) => {
287
+ debug('fileHandler', fileHandler.type);
288
+ importData[fileHandler.type] = await fileHandler.loadFile([_.pick(file, 'name', 'path')]);
289
+ }));
290
+
291
+ return importData;
285
292
  }
286
293
 
287
294
  /**
@@ -305,6 +312,7 @@ class ImportManager {
305
312
  * @returns {Promise<ImportData>}
306
313
  */
307
314
  async preProcess(importData) {
315
+ debug('preProcess');
308
316
  for (const importer of this.importers) {
309
317
  importData = importer.preProcess(importData);
310
318
  }
@@ -321,10 +329,12 @@ class ImportManager {
321
329
  * @returns {Promise<Object.<string, ImportResult>>} importResults
322
330
  */
323
331
  async doImport(importData, importOptions) {
332
+ debug('doImport', this.importers);
324
333
  importOptions = importOptions || {};
325
334
  const importResults = {};
326
335
 
327
336
  for (const importer of this.importers) {
337
+ debug('importer looking for', importer.type, 'in', Object.keys(importData));
328
338
  if (Object.prototype.hasOwnProperty.call(importData, importer.type)) {
329
339
  importResults[importer.type] = await importer.doImport(importData[importer.type], importOptions);
330
340
  }
@@ -411,6 +421,8 @@ class ImportManager {
411
421
  importData = await this.loadFile(file);
412
422
  }
413
423
 
424
+ debug('importFromFile completed file load', importData);
425
+
414
426
  const env = config.get('env');
415
427
  if (!env?.startsWith('testing') && !importOptions.runningInJob) {
416
428
  return jobManager.addJob({
@@ -14,6 +14,7 @@ const ProductsImporter = require('./products');
14
14
  const StripeProductsImporter = require('./stripe-products');
15
15
  const StripePricesImporter = require('./stripe-prices');
16
16
  const CustomThemeSettingsImporter = require('./custom-theme-settings');
17
+ const RevueSubscriberImporter = require('./revue-subscriber');
17
18
  const RolesImporter = require('./roles');
18
19
  const {slugify} = require('@tryghost/string/lib');
19
20
 
@@ -24,6 +25,7 @@ DataImporter = {
24
25
  type: 'data',
25
26
 
26
27
  preProcess: function preProcess(importData) {
28
+ debug('preProcess');
27
29
  importData.preProcessedByData = true;
28
30
  return importData;
29
31
  },
@@ -39,12 +41,14 @@ DataImporter = {
39
41
  importers.stripe_prices = new StripePricesImporter(importData.data);
40
42
  importers.posts = new PostsImporter(importData.data);
41
43
  importers.custom_theme_settings = new CustomThemeSettingsImporter(importData.data);
44
+ importers.revue_subscribers = new RevueSubscriberImporter(importData.data);
42
45
 
43
46
  return importData;
44
47
  },
45
48
 
46
49
  // Allow importing with an options object that is passed through the importer
47
50
  doImport: async function doImport(importData, importOptions) {
51
+ debug('doImport');
48
52
  importOptions = importOptions || {};
49
53
 
50
54
  if (importOptions.importTag && importData?.data?.posts) {
@@ -55,7 +59,7 @@ DataImporter = {
55
59
  importData.data.tags.push({
56
60
  id: tagId,
57
61
  name: importOptions.importTag,
58
- slug: slugify(importOptions.importTag)
62
+ slug: slugify(importOptions.importTag.replace(/^#/, 'hash-'))
59
63
  });
60
64
  if (!('posts_tags' in importData.data)) {
61
65
  importData.data.posts_tags = [];
@@ -272,7 +272,11 @@ class PostsImporter extends BaseImporter {
272
272
 
273
273
  model.mobiledoc = JSON.stringify(mobiledoc);
274
274
  model.html = mobiledocLib.mobiledocHtmlRenderer.render(JSON.parse(model.mobiledoc));
275
+ } else if (model.html) {
276
+ model.mobiledoc = JSON.stringify(mobiledocLib.htmlToMobiledocConverter(model.html));
277
+ model.html = mobiledocLib.mobiledocHtmlRenderer.render(JSON.parse(model.mobiledoc));
275
278
  }
279
+
276
280
  this.sanitizePostsMeta(model);
277
281
  });
278
282
 
@@ -0,0 +1,55 @@
1
+ const debug = require('@tryghost/debug')('importer:revue-subscriber');
2
+ const BaseImporter = require('./base');
3
+
4
+ const papaparse = require('papaparse');
5
+ const path = require('path');
6
+ const fs = require('fs-extra');
7
+
8
+ const config = require('../../../../../shared/config');
9
+ const models = require('../../../../models');
10
+
11
+ class RevueSubscriberImporter extends BaseImporter {
12
+ constructor(allDataFromFile) {
13
+ super(allDataFromFile, {
14
+ modelName: 'Member',
15
+ dataKeyToImport: 'revue_subscribers'
16
+ });
17
+ }
18
+
19
+ beforeImport() {
20
+ debug('beforeImport');
21
+ return super.beforeImport();
22
+ }
23
+
24
+ async doImport(options, importOptions) {
25
+ debug('doImport', this.modelName, this.dataToImport.length);
26
+
27
+ // Don't do anything if there is no data to import
28
+ if (this.dataToImport.length === 0) {
29
+ return Promise.resolve();
30
+ }
31
+
32
+ // required here rather than top-level to avoid pulling in before it's initialized during boot
33
+ const membersService = require('../../../../services/members');
34
+
35
+ const importLabel = importOptions.importTag ? importOptions.importTag.replace(/^#/, '') : null;
36
+
37
+ const outputFileName = `Converted ${importLabel}.csv`;
38
+ const outputFilePath = path.join(config.getContentPath('data'), '/', outputFileName);
39
+ const csvData = papaparse.unparse(this.dataToImport);
40
+
41
+ const memberImporterOptions = {
42
+ pathToCSV: outputFilePath,
43
+ globalLabels: [{name: importLabel}],
44
+ importLabel: {name: importLabel},
45
+ LabelModel: models.Label,
46
+ forceInline: true
47
+ };
48
+
49
+ await fs.writeFile(outputFilePath, csvData);
50
+
51
+ return membersService.processImport(memberImporterOptions);
52
+ }
53
+ }
54
+
55
+ module.exports = RevueSubscriberImporter;
@@ -571,19 +571,25 @@ async function createEmailBatches({emailModel, memberRows, memberSegment, option
571
571
  return batchIds;
572
572
  }
573
573
 
574
- const statusChangedHandler = (emailModel, options) => {
574
+ const statusChangedHandler = async (emailModel, options) => {
575
575
  const emailRetried = emailModel.wasChanged()
576
576
  && emailModel.get('status') === 'pending'
577
577
  && emailModel.previous('status') === 'failed';
578
578
 
579
579
  if (emailRetried) {
580
- pendingEmailHandler(emailModel, options);
580
+ await pendingEmailHandler(emailModel, options);
581
581
  }
582
582
  };
583
583
 
584
584
  function listen() {
585
- events.on('email.added', pendingEmailHandler);
586
- events.on('email.edited', statusChangedHandler);
585
+ events.on('email.added', (emailModel, options) => pendingEmailHandler(emailModel, options).catch((e) => {
586
+ logging.error('Error in email.added event handler');
587
+ logging.error(e);
588
+ }));
589
+ events.on('email.edited', (emailModel, options) => statusChangedHandler(emailModel, options).catch((e) => {
590
+ logging.error('Error in email.edited event handler');
591
+ logging.error(e);
592
+ }));
587
593
  }
588
594
 
589
595
  // Public API
@@ -77,7 +77,10 @@ const deleteSession = async function (req, res) {
77
77
  res.writeHead(204);
78
78
  res.end();
79
79
  } catch (err) {
80
- res.writeHead(err.statusCode, {
80
+ if (!err.statusCode) {
81
+ logging.error(err);
82
+ }
83
+ res.writeHead(err.statusCode ?? 500, {
81
84
  'Content-Type': 'text/plain;charset=UTF-8'
82
85
  });
83
86
  res.end(err.message);
@@ -101,11 +104,20 @@ const getMemberData = async function (req, res) {
101
104
  const deleteSuppression = async function (req, res) {
102
105
  try {
103
106
  const member = await membersService.ssr.getMemberDataFromSession(req, res);
107
+ const options = {
108
+ id: member.id,
109
+ withRelated: ['newsletters']
110
+ };
104
111
  await emailSuppressionList.removeEmail(member.email);
112
+ await membersService.api.members.update({subscribed: true}, options);
113
+
105
114
  res.writeHead(204);
106
115
  res.end();
107
116
  } catch (err) {
108
- res.writeHead(err.statusCode, {
117
+ if (!err.statusCode) {
118
+ logging.error(err);
119
+ }
120
+ res.writeHead(err.statusCode ?? 500, {
109
121
  'Content-Type': 'text/plain;charset=UTF-8'
110
122
  });
111
123
  res.end(err.message);
@@ -188,7 +200,10 @@ const updateMemberData = async function (req, res) {
188
200
  res.json(null);
189
201
  }
190
202
  } catch (err) {
191
- res.writeHead(err.statusCode, {
203
+ if (!err.statusCode) {
204
+ logging.error(err);
205
+ }
206
+ res.writeHead(err.statusCode ?? 500, {
192
207
  'Content-Type': 'text/plain;charset=UTF-8'
193
208
  });
194
209
  res.end(err.message);
@@ -23,13 +23,16 @@ class MembersEventsServiceWrapper {
23
23
  labsService
24
24
  });
25
25
 
26
+ const db = require('../../data/db');
27
+
26
28
  this.lastSeenAtUpdater = new LastSeenAtUpdater({
27
29
  services: {
28
30
  settingsCache
29
31
  },
30
32
  getMembersApi() {
31
33
  return members.api;
32
- }
34
+ },
35
+ db
33
36
  });
34
37
 
35
38
  this.eventStorage.subscribe(DomainEvents);
@@ -22,7 +22,9 @@ async function configureApi() {
22
22
  }
23
23
 
24
24
  const debouncedConfigureApi = _.debounce(() => {
25
- configureApi();
25
+ configureApi().catch((err) => {
26
+ logging.error(err);
27
+ });
26
28
  }, 600);
27
29
 
28
30
  module.exports = new StripeService({
@@ -58,11 +58,7 @@
58
58
  },
59
59
  "transports": [
60
60
  "stdout"
61
- ],
62
- "slowHelper": {
63
- "level": "warn",
64
- "threshold": 200
65
- }
61
+ ]
66
62
  },
67
63
  "spam": {
68
64
  "user_login": {
@@ -145,6 +141,18 @@
145
141
  "maxAge": 0
146
142
  }
147
143
  },
144
+ "optimization": {
145
+ "getHelper": {
146
+ "timeout": {
147
+ "threshold": 5000,
148
+ "level": "error"
149
+ },
150
+ "notify": {
151
+ "threshold": 200,
152
+ "level": "warn"
153
+ }
154
+ }
155
+ },
148
156
  "imageOptimization": {
149
157
  "resize": true,
150
158
  "srcsets": true
@@ -161,7 +169,7 @@
161
169
  },
162
170
  "portal": {
163
171
  "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
164
- "version": "2.21"
172
+ "version": "2.22"
165
173
  },
166
174
  "sodoSearch": {
167
175
  "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",