ghost 4.22.1 → 4.23.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 (128) hide show
  1. package/.c8rc.json +24 -0
  2. package/.eslintrc.js +6 -0
  3. package/Gruntfile.js +1 -1
  4. package/content/public/README.md +3 -0
  5. package/core/boot.js +20 -12
  6. package/core/built/assets/{chunk.3.1148677ff3b78e5aeaee.js → chunk.3.8f95b516d88ff4eec64c.js} +18 -18
  7. package/core/built/assets/{ghost-dark-684ad238e1a858c7cb5be6988de7c6f5.css → ghost-dark-42cf6e0c730578940ec069bda45aea41.css} +1 -1
  8. package/core/built/assets/{ghost.min-f7037eca328f4d4eb99f0309c19c9bae.js → ghost.min-cccc107e881b74c7aaf1a73e1e5e0dee.js} +189 -143
  9. package/core/built/assets/{ghost.min-66e08535f8bb797a8c40e0a2b31f1e9e.css → ghost.min-fcf6a0738421f86c47c55f20d00c5ba9.css} +1 -1
  10. package/core/built/assets/icons/powered-by-tenor.svg +35 -0
  11. package/core/built/assets/icons/tenor.svg +7 -0
  12. package/core/built/assets/{vendor.min-7c8fdd90f7ecd2e94328a07ea3b64608.js → vendor.min-c9002845b6c30ac978abdadde9f33d7c.js} +8189 -7601
  13. package/core/frontend/apps/amp/lib/views/amp.hbs +104 -0
  14. package/core/frontend/apps/private-blogging/lib/router.js +1 -1
  15. package/core/frontend/services/card-assets/service.js +21 -13
  16. package/core/frontend/services/routing/CollectionRouter.js +4 -5
  17. package/core/frontend/services/routing/EmailRouter.js +1 -1
  18. package/core/frontend/services/routing/ParentRouter.js +0 -8
  19. package/core/frontend/services/routing/PreviewRouter.js +1 -1
  20. package/core/frontend/services/routing/StaticPagesRouter.js +1 -1
  21. package/core/frontend/services/routing/StaticRoutesRouter.js +4 -4
  22. package/core/frontend/services/routing/TaxonomyRouter.js +3 -3
  23. package/core/frontend/services/routing/{middlewares → middleware}/index.js +0 -0
  24. package/core/frontend/services/routing/{middlewares → middleware}/page-param.js +0 -0
  25. package/core/frontend/services/routing/router-manager.js +7 -2
  26. package/core/frontend/services/rss/generate-feed.js +2 -1
  27. package/core/frontend/src/cards/css/bookmark.css +66 -48
  28. package/core/frontend/src/cards/css/button.css +30 -0
  29. package/core/frontend/src/cards/css/callout.css +50 -0
  30. package/core/frontend/src/cards/css/gallery.css +8 -13
  31. package/core/frontend/src/cards/css/nft.css +94 -0
  32. package/core/frontend/src/cards/css/toggle.css +47 -0
  33. package/core/frontend/src/cards/js/toggle.js +16 -0
  34. package/core/frontend/web/middleware/serve-public-file.js +14 -8
  35. package/core/frontend/web/routes.js +0 -1
  36. package/core/frontend/web/site.js +15 -12
  37. package/core/server/adapters/storage/LocalFilesStorage.js +17 -0
  38. package/core/server/adapters/storage/LocalImagesStorage.js +1 -0
  39. package/core/server/adapters/storage/LocalMediaStorage.js +2 -1
  40. package/core/server/adapters/storage/LocalStorageBase.js +30 -5
  41. package/core/server/api/canary/authentication.js +1 -1
  42. package/core/server/api/canary/files.js +19 -0
  43. package/core/server/api/canary/index.js +4 -0
  44. package/core/server/api/canary/media.js +25 -5
  45. package/core/server/api/canary/oembed.js +3 -0
  46. package/core/server/api/canary/utils/serializers/input/index.js +4 -0
  47. package/core/server/api/canary/utils/serializers/input/media.js +8 -0
  48. package/core/server/api/canary/utils/serializers/output/config.js +21 -14
  49. package/core/server/api/canary/utils/serializers/output/files.js +27 -0
  50. package/core/server/api/canary/utils/serializers/output/index.js +4 -0
  51. package/core/server/api/canary/utils/serializers/output/media.js +9 -0
  52. package/core/server/api/canary/utils/validators/input/files.js +7 -0
  53. package/core/server/api/canary/utils/validators/input/index.js +4 -0
  54. package/core/server/api/canary/utils/validators/input/media.js +4 -0
  55. package/core/server/api/v2/authentication.js +1 -1
  56. package/core/server/api/v3/authentication.js +1 -1
  57. package/core/server/data/db/connection.js +7 -0
  58. package/core/server/data/importer/importers/data/data-importer.js +3 -3
  59. package/core/server/data/migrations/init/2-create-fixtures.js +3 -20
  60. package/core/server/data/migrations/versions/1.21/1-add-contributor-role.js +5 -5
  61. package/core/server/data/migrations/versions/2.15/2-insert-zapier-integration.js +3 -3
  62. package/core/server/data/migrations/versions/2.2/3-insert-admin-integration-role.js +5 -5
  63. package/core/server/data/migrations/versions/2.27/1-insert-ghost-db-backup-role.js +5 -6
  64. package/core/server/data/migrations/versions/2.27/2-insert-db-backup-integration.js +3 -4
  65. package/core/server/data/migrations/versions/2.28/3-insert-ghost-scheduler-role.js +7 -7
  66. package/core/server/data/migrations/versions/2.28/4-insert-scheduler-integration.js +3 -3
  67. package/core/server/data/migrations/versions/4.23/01-truncate-offer-names.js +58 -0
  68. package/core/server/data/schema/fixtures/fixture-manager.js +340 -0
  69. package/core/server/data/schema/fixtures/index.js +8 -2
  70. package/core/server/services/mega/post-email-serializer.js +5 -1
  71. package/core/server/services/mega/segment-parser.js +1 -2
  72. package/core/server/services/mega/template.js +69 -1
  73. package/core/server/services/nft-oembed.js +57 -0
  74. package/core/server/services/oembed.js +161 -126
  75. package/core/server/services/public-config/config.js +2 -1
  76. package/core/server/services/stripe/index.js +4 -2
  77. package/core/server/services/url/Resource.js +1 -1
  78. package/core/server/services/url/Resources.js +36 -23
  79. package/core/server/services/url/UrlGenerator.js +23 -20
  80. package/core/server/services/url/UrlService.js +123 -21
  81. package/core/server/services/url/Urls.js +7 -2
  82. package/core/server/services/url/index.js +9 -1
  83. package/core/server/web/admin/app.js +6 -6
  84. package/core/server/web/admin/views/default-prod.html +4 -4
  85. package/core/server/web/admin/views/default.html +4 -4
  86. package/core/server/web/api/app.js +1 -1
  87. package/core/server/web/api/canary/admin/app.js +4 -4
  88. package/core/server/web/api/canary/admin/middleware.js +6 -6
  89. package/core/server/web/api/canary/admin/routes.js +20 -5
  90. package/core/server/web/api/canary/content/app.js +4 -4
  91. package/core/server/web/api/canary/content/middleware.js +3 -3
  92. package/core/server/web/api/middleware/cors.js +7 -7
  93. package/core/server/web/api/v2/admin/app.js +4 -4
  94. package/core/server/web/api/v2/admin/middleware.js +6 -6
  95. package/core/server/web/api/v2/admin/routes.js +5 -5
  96. package/core/server/web/api/v2/content/app.js +4 -4
  97. package/core/server/web/api/v2/content/middleware.js +3 -3
  98. package/core/server/web/api/v3/admin/app.js +4 -4
  99. package/core/server/web/api/v3/admin/middleware.js +6 -6
  100. package/core/server/web/api/v3/admin/routes.js +5 -5
  101. package/core/server/web/api/v3/content/app.js +4 -4
  102. package/core/server/web/api/v3/content/middleware.js +3 -3
  103. package/core/server/web/members/app.js +7 -7
  104. package/core/server/web/oauth/app.js +1 -1
  105. package/core/server/web/parent/app.js +2 -3
  106. package/core/server/web/parent/frontend.js +1 -1
  107. package/core/server/web/shared/index.js +2 -2
  108. package/core/server/web/shared/{middlewares → middleware}/api/index.js +0 -0
  109. package/core/server/web/shared/{middlewares → middleware}/api/spam-prevention.js +0 -0
  110. package/core/server/web/shared/{middlewares → middleware}/brute.js +0 -0
  111. package/core/server/web/shared/{middlewares → middleware}/cache-control.js +0 -0
  112. package/core/server/web/shared/{middlewares → middleware}/error-handler.js +0 -0
  113. package/core/server/web/shared/{middlewares → middleware}/index.js +0 -0
  114. package/core/server/web/shared/{middlewares → middleware}/maintenance.js +0 -0
  115. package/core/server/web/shared/{middlewares → middleware}/pretty-urls.js +0 -0
  116. package/core/server/web/shared/{middlewares → middleware}/uncapitalise.js +0 -0
  117. package/core/server/web/shared/{middlewares → middleware}/url-redirects.js +0 -0
  118. package/core/shared/config/defaults.json +10 -2
  119. package/core/shared/config/helpers.js +44 -0
  120. package/core/shared/config/loader.js +1 -1
  121. package/core/shared/config/overrides.json +2 -2
  122. package/core/shared/labs.js +8 -1
  123. package/loggingrc.js +19 -20
  124. package/package.json +35 -35
  125. package/urls.json +597 -0
  126. package/yarn.lock +655 -339
  127. package/core/server/data/schema/fixtures/utils.js +0 -321
  128. package/core/server/web/parent/vhost-utils.js +0 -39
@@ -0,0 +1,94 @@
1
+ .kg-nft-card {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ width: 100%;
6
+ }
7
+
8
+ .kg-nft-card-container {
9
+ position: static;
10
+ display: flex;
11
+ flex: auto;
12
+ flex-direction: column;
13
+ text-decoration: none;
14
+ font-family: -apple-system, BlinkMacSystemFont,
15
+ 'avenir next', avenir,
16
+ 'helvetica neue', helvetica,
17
+ ubuntu,
18
+ roboto, noto,
19
+ 'segoe ui', arial,
20
+ sans-serif;
21
+ font-size: 1.4rem;
22
+ font-weight: 400;
23
+ box-shadow: 0 2px 6px -2px rgb(0 0 0 / 10%), 0 0 1px rgb(0 0 0 / 40%);
24
+ width: 100%;
25
+ max-width: 512px;
26
+ color: #222;
27
+ background: #fff;
28
+ border-radius: 5px;
29
+ transition: none;
30
+ }
31
+
32
+ .kg-nft-card:hover {
33
+ color: #333;
34
+ opacity: 1.0;
35
+ transition: none;
36
+ }
37
+
38
+ .kg-nft-card * {
39
+ position: static;
40
+ }
41
+
42
+ .kg-nft-metadata {
43
+ padding: 2.0rem;
44
+ width: 100%;
45
+ }
46
+
47
+ .kg-nft-image {
48
+ border-radius: 5px 5px 0 0;
49
+ width: 100%;
50
+ }
51
+
52
+ .kg-nft-header {
53
+ display: flex;
54
+ justify-content: space-between;
55
+ align-items: flex-start;
56
+ gap: 20px;
57
+ }
58
+
59
+ .kg-nft-header h4.kg-nft-title {
60
+ font-family: inherit;
61
+ font-size: 1.9rem;
62
+ font-weight: 700;
63
+ line-height: 1.3em;
64
+ min-width: unset;
65
+ max-width: unset;
66
+ margin: 0;
67
+ color: #222;
68
+ }
69
+
70
+ .kg-nft-opensea-logo {
71
+ margin-top: 2px;
72
+ width: 100px;
73
+ object-fit: scale-down;
74
+ }
75
+
76
+ .kg-nft-creator {
77
+ font-family: inherit;
78
+ line-height: 1.4em;
79
+ margin: 0.4rem 0 0;
80
+ color: #ababab;
81
+ }
82
+
83
+ .kg-nft-creator span {
84
+ font-weight: 500;
85
+ color: #222;
86
+ }
87
+
88
+ .kg-nft-card p.kg-nft-description {
89
+ font-family: inherit;
90
+ font-size: 1.4rem;
91
+ line-height: 1.4em;
92
+ margin: 2.0rem 0 0;
93
+ color: #222;
94
+ }
@@ -0,0 +1,47 @@
1
+ .kg-toggle-card[data-kg-toggle-state="close"] .kg-toggle-content{
2
+ visibility: hidden;
3
+ opacity: 0;
4
+ height: 0;
5
+ padding: 0;
6
+ }
7
+
8
+ .kg-toggle-card[data-kg-toggle-state="close"] svg {
9
+ transform: unset;
10
+ }
11
+
12
+ .kg-toggle-card {
13
+ border: 1px solid rgba(127, 127, 127, 0.15);
14
+ border-radius: 4px;
15
+ padding: 20px;
16
+ }
17
+
18
+ .kg-toggle-heading {
19
+ font-size: 2rem;
20
+ font-weight: 600;
21
+ cursor: pointer;
22
+ display: flex;
23
+ justify-content: space-between;
24
+ align-items: flex-start;
25
+ }
26
+
27
+ .kg-toggle-card-icon {
28
+ height: 24px;
29
+ width: 24px;
30
+ display: flex;
31
+ justify-content: center;
32
+ align-items: center;
33
+ margin-left: 16px;
34
+ }
35
+
36
+ .kg-toggle-heading svg {
37
+ width: 14px;
38
+ color: rgba(127, 127, 127, 0.4);
39
+ transition: transform 0.3s;
40
+ transform: rotate(180deg);
41
+ }
42
+
43
+ .kg-toggle-content {
44
+ display: flex;
45
+ transition: opacity 0.3s;
46
+ padding-top: 8px;
47
+ }
@@ -0,0 +1,16 @@
1
+ const toggleHeadingElements = document.getElementsByClassName("kg-toggle-heading");
2
+
3
+ const toggleFn = function(event) {
4
+ const targetElement = event.target;
5
+ const parentElement = targetElement.closest('.kg-toggle-card');
6
+ var toggleState = parentElement.getAttribute("data-kg-toggle-state");
7
+ if (toggleState === 'close') {
8
+ parentElement.setAttribute('data-kg-toggle-state', 'open');
9
+ } else {
10
+ parentElement.setAttribute('data-kg-toggle-state', 'close');
11
+ }
12
+ };
13
+
14
+ for (let i = 0; i < toggleHeadingElements.length; i++) {
15
+ toggleHeadingElements[i].addEventListener('click', toggleFn, false);
16
+ }
@@ -11,10 +11,16 @@ const messages = {
11
11
  fileNotFound: 'File not found'
12
12
  };
13
13
 
14
- function createPublicFileMiddleware(file, type, maxAge) {
14
+ function createPublicFileMiddleware(location, file, mime, maxAge) {
15
15
  let content;
16
- const publicFilePath = config.get('paths').publicFilePath;
17
- const filePath = file.match(/^public/) ? path.join(publicFilePath, file.replace(/^public/, '')) : path.join(publicFilePath, file);
16
+ // These files are provided by Ghost, and therefore live inside of the core folder
17
+ const staticFilePath = config.get('paths').publicFilePath;
18
+ // These files are built on the fly, and must be saved in the content folder
19
+ const builtFilePath = config.getContentPath('public');
20
+
21
+ let locationPath = location === 'static' ? staticFilePath : builtFilePath;
22
+
23
+ const filePath = file.match(/^public/) ? path.join(locationPath, file.replace(/^public/, '')) : path.join(locationPath, file);
18
24
  const blogRegex = /(\{\{blog-url\}\})/g;
19
25
 
20
26
  return function servePublicFileMiddleware(req, res, next) {
@@ -24,7 +30,7 @@ function createPublicFileMiddleware(file, type, maxAge) {
24
30
  }
25
31
 
26
32
  // send image files directly and let express handle content-length, etag, etc
27
- if (type.match(/^image/)) {
33
+ if (mime.match(/^image/)) {
28
34
  return res.sendFile(filePath, (err) => {
29
35
  if (err && err.status === 404) {
30
36
  // ensure we're triggering basic asset 404 and not a templated 404
@@ -57,13 +63,13 @@ function createPublicFileMiddleware(file, type, maxAge) {
57
63
 
58
64
  let str = buf.toString();
59
65
 
60
- if (type === 'text/xsl' || type === 'text/plain' || type === 'application/javascript') {
66
+ if (mime === 'text/xsl' || mime === 'text/plain' || mime === 'application/javascript') {
61
67
  str = str.replace(blogRegex, urlUtils.urlFor('home', true).replace(/\/$/, ''));
62
68
  }
63
69
 
64
70
  content = {
65
71
  headers: {
66
- 'Content-Type': type,
72
+ 'Content-Type': mime,
67
73
  'Content-Length': Buffer.from(str).length,
68
74
  ETag: `"${crypto.createHash('md5').update(str, 'utf8').digest('hex')}"`,
69
75
  'Cache-Control': `public, max-age=${maxAge}`
@@ -78,8 +84,8 @@ function createPublicFileMiddleware(file, type, maxAge) {
78
84
 
79
85
  // ### servePublicFile Middleware
80
86
  // Handles requests to robots.txt and favicon.ico (and caches them)
81
- function servePublicFile(file, type, maxAge) {
82
- const publicFileMiddleware = createPublicFileMiddleware(file, type, maxAge);
87
+ function servePublicFile(location, file, type, maxAge) {
88
+ const publicFileMiddleware = createPublicFileMiddleware(location, file, type, maxAge);
83
89
 
84
90
  return function servePublicFileMiddleware(req, res, next) {
85
91
  if (req.path === '/' + file) {
@@ -1,7 +1,6 @@
1
1
  const debug = require('@tryghost/debug')('routing');
2
2
 
3
3
  const routing = require('../services/routing');
4
- // NOTE: temporary import from the frontend, will become a backend service soon
5
4
  const urlService = require('../../server/services/url');
6
5
  const routeSettings = require('../../server/services/route-settings');
7
6
 
@@ -25,6 +25,7 @@ const labs = require('../../shared/labs');
25
25
 
26
26
  const STATIC_IMAGE_URL_PREFIX = `/${urlUtils.STATIC_IMAGE_URL_PREFIX}`;
27
27
  const STATIC_MEDIA_URL_PREFIX = `/${constants.STATIC_MEDIA_URL_PREFIX}`;
28
+ const STATIC_FILES_URL_PREFIX = `/${constants.STATIC_FILES_URL_PREFIX}`;
28
29
 
29
30
  let router;
30
31
 
@@ -103,20 +104,22 @@ module.exports = function setupSiteApp(options = {}) {
103
104
  siteApp.use(mw.serveFavicon());
104
105
 
105
106
  // Serve sitemap.xsl file
106
- siteApp.use(mw.servePublicFile('sitemap.xsl', 'text/xsl', constants.ONE_DAY_S));
107
+ siteApp.use(mw.servePublicFile('static', 'sitemap.xsl', 'text/xsl', constants.ONE_DAY_S));
107
108
 
108
109
  // Serve stylesheets for default templates
109
- siteApp.use(mw.servePublicFile('public/ghost.css', 'text/css', constants.ONE_HOUR_S));
110
- siteApp.use(mw.servePublicFile('public/ghost.min.css', 'text/css', constants.ONE_YEAR_S));
110
+ siteApp.use(mw.servePublicFile('static', 'public/ghost.css', 'text/css', constants.ONE_HOUR_S));
111
+ siteApp.use(mw.servePublicFile('static', 'public/ghost.min.css', 'text/css', constants.ONE_YEAR_S));
111
112
 
112
113
  // Card assets
113
- siteApp.use(mw.servePublicFile('public/cards.min.css', 'text/css', constants.ONE_YEAR_S));
114
- siteApp.use(mw.servePublicFile('public/cards.min.js', 'text/js', constants.ONE_YEAR_S));
114
+ siteApp.use(mw.servePublicFile('built', 'public/cards.min.css', 'text/css', constants.ONE_YEAR_S));
115
+ siteApp.use(mw.servePublicFile('built', 'public/cards.min.js', 'text/js', constants.ONE_YEAR_S));
115
116
 
116
117
  // Serve blog images using the storage adapter
117
118
  siteApp.use(STATIC_IMAGE_URL_PREFIX, mw.handleImageSizes, storage.getStorage('images').serve());
118
119
  // Serve blog media using the storage adapter
119
120
  siteApp.use(STATIC_MEDIA_URL_PREFIX, labs.enabledMiddleware('mediaAPI'), storage.getStorage('media').serve());
121
+ // Serve blog files using the storage adapter
122
+ siteApp.use(STATIC_FILES_URL_PREFIX, labs.enabledMiddleware('filesAPI'), storage.getStorage('files').serve());
120
123
 
121
124
  // Global handling for member session, ensures a member is logged in to the frontend
122
125
  siteApp.use(membersService.middleware.loadMemberSession);
@@ -144,26 +147,26 @@ module.exports = function setupSiteApp(options = {}) {
144
147
  debug('Themes done');
145
148
 
146
149
  // Serve robots.txt if not found in theme
147
- siteApp.use(mw.servePublicFile('robots.txt', 'text/plain', constants.ONE_HOUR_S));
150
+ siteApp.use(mw.servePublicFile('static', 'robots.txt', 'text/plain', constants.ONE_HOUR_S));
148
151
 
149
152
  // site map - this should probably be refactored to be an internal app
150
153
  sitemapHandler(siteApp);
151
154
  debug('Internal apps done');
152
155
 
153
156
  // send 503 error page in case of maintenance
154
- siteApp.use(shared.middlewares.maintenance);
157
+ siteApp.use(shared.middleware.maintenance);
155
158
 
156
159
  // Add in all trailing slashes & remove uppercase
157
160
  // must happen AFTER asset loading and BEFORE routing
158
- siteApp.use(shared.middlewares.prettyUrls);
161
+ siteApp.use(shared.middleware.prettyUrls);
159
162
 
160
163
  // ### Caching
161
164
  siteApp.use(function (req, res, next) {
162
165
  // Site frontend is cacheable UNLESS request made by a member or blog is in private mode
163
166
  if (req.member || res.isPrivateBlog) {
164
- return shared.middlewares.cacheControl('private')(req, res, next);
167
+ return shared.middleware.cacheControl('private')(req, res, next);
165
168
  } else {
166
- return shared.middlewares.cacheControl('public', {maxAge: config.get('caching:frontend:maxAge')})(req, res, next);
169
+ return shared.middleware.cacheControl('public', {maxAge: config.get('caching:frontend:maxAge')})(req, res, next);
167
170
  }
168
171
  });
169
172
 
@@ -176,7 +179,7 @@ module.exports = function setupSiteApp(options = {}) {
176
179
  siteApp.use(SiteRouter);
177
180
 
178
181
  // ### Error handlers
179
- siteApp.use(shared.middlewares.errorHandler.pageNotFound);
182
+ siteApp.use(shared.middleware.errorHandler.pageNotFound);
180
183
  config.get('apps:internal').forEach((appName) => {
181
184
  const app = require(path.join(config.get('paths').internalAppPath, appName));
182
185
 
@@ -184,7 +187,7 @@ module.exports = function setupSiteApp(options = {}) {
184
187
  app.setupErrorHandling(siteApp);
185
188
  }
186
189
  });
187
- siteApp.use(shared.middlewares.errorHandler.handleThemeResponse);
190
+ siteApp.use(shared.middleware.errorHandler.handleThemeResponse);
188
191
 
189
192
  debug('Site setup end');
190
193
 
@@ -0,0 +1,17 @@
1
+ // # Local File System Storage module
2
+ // The (default) module for storing media, using the local file system
3
+ const config = require('../../../shared/config');
4
+ const constants = require('@tryghost/constants');
5
+ const LocalStorageBase = require('./LocalStorageBase');
6
+
7
+ class LocalFilesStorage extends LocalStorageBase {
8
+ constructor() {
9
+ super({
10
+ storagePath: config.getContentPath('files'),
11
+ siteUrl: config.getSiteUrl(),
12
+ staticFileURLPrefix: constants.STATIC_FILES_URL_PREFIX
13
+ });
14
+ }
15
+ }
16
+
17
+ module.exports = LocalFilesStorage;
@@ -19,6 +19,7 @@ class LocalImagesStorage extends LocalStorageBase {
19
19
  super({
20
20
  storagePath: config.getContentPath('images'),
21
21
  staticFileURLPrefix: urlUtils.STATIC_IMAGE_URL_PREFIX,
22
+ siteUrl: config.getSiteUrl(),
22
23
  errorMessages: messages
23
24
  });
24
25
  }
@@ -1,4 +1,4 @@
1
- // # Local File System Video Storage module
1
+ // # Local File System Media Storage module
2
2
  // The (default) module for storing media, using the local file system
3
3
  const config = require('../../../shared/config');
4
4
  const constants = require('@tryghost/constants');
@@ -15,6 +15,7 @@ class LocalMediaStore extends LocalStorageBase {
15
15
  super({
16
16
  storagePath: config.getContentPath('media'),
17
17
  staticFileURLPrefix: constants.STATIC_MEDIA_URL_PREFIX,
18
+ siteUrl: config.getSiteUrl(),
18
19
  errorMessages: messages
19
20
  });
20
21
  }
@@ -16,7 +16,8 @@ const StorageBase = require('ghost-storage-base');
16
16
  const messages = {
17
17
  notFound: 'File not found',
18
18
  notFoundWithRef: 'File not found: {file}',
19
- cannotRead: 'Could not read file: {file}'
19
+ cannotRead: 'Could not read file: {file}',
20
+ invalidUrlParameter: `The URL "{url}" is not a valid URL for this site.`
20
21
  };
21
22
 
22
23
  class LocalStorageBase extends StorageBase {
@@ -24,17 +25,20 @@ class LocalStorageBase extends StorageBase {
24
25
  *
25
26
  * @param {Object} options
26
27
  * @param {String} options.storagePath
28
+ * @param {String} options.siteUrl
27
29
  * @param {String} [options.staticFileURLPrefix]
28
30
  * @param {Object} [options.errorMessages]
29
31
  * @param {String} [options.errorMessages.notFound]
30
32
  * @param {String} [options.errorMessages.notFoundWithRef]
31
33
  * @param {String} [options.errorMessages.cannotRead]
32
34
  */
33
- constructor({storagePath, staticFileURLPrefix, errorMessages}) {
35
+ constructor({storagePath, staticFileURLPrefix, siteUrl, errorMessages}) {
34
36
  super();
35
37
 
36
38
  this.storagePath = storagePath;
37
39
  this.staticFileURLPrefix = staticFileURLPrefix;
40
+ this.siteUrl = siteUrl;
41
+ this.staticFileUrl = `${siteUrl}${staticFileURLPrefix}`;
38
42
  this.errorMessages = errorMessages || messages;
39
43
  }
40
44
 
@@ -71,6 +75,26 @@ class LocalStorageBase extends StorageBase {
71
75
  return fullUrl;
72
76
  }
73
77
 
78
+ /**
79
+ *
80
+ * @param {String} url full url under which the stored content is served, result of save method
81
+ * @returns {String} path under which the content is stored
82
+ */
83
+ urlToPath(url) {
84
+ let filePath;
85
+
86
+ if (url.match(this.staticFileUrl)) {
87
+ filePath = url.replace(this.staticFileUrl, '');
88
+ filePath = path.join(this.storagePath, filePath);
89
+ } else {
90
+ throw new errors.IncorrectUsageError({
91
+ message: tpl(messages.invalidUrlParameter, {url})
92
+ });
93
+ }
94
+
95
+ return filePath;
96
+ }
97
+
74
98
  exists(fileName, targetDir) {
75
99
  const filePath = path.join(targetDir || this.storagePath, fileName);
76
100
 
@@ -132,11 +156,12 @@ class LocalStorageBase extends StorageBase {
132
156
  }
133
157
 
134
158
  /**
135
- * Not implemented.
159
+ * @param {String} filePath
136
160
  * @returns {Promise.<*>}
137
161
  */
138
- delete() {
139
- return Promise.reject('not implemented');
162
+ async delete(fileName, targetDir) {
163
+ const filePath = path.join(targetDir, fileName);
164
+ return await fs.remove(filePath);
140
165
  }
141
166
 
142
167
  /**
@@ -163,7 +163,7 @@ module.exports = {
163
163
  options = Object.assign(options, {context: {internal: true}});
164
164
  return auth.passwordreset.doReset(options, tokenParts, api.settings)
165
165
  .then((params) => {
166
- web.shared.middlewares.api.spamPrevention.userLogin().reset(frame.options.ip, `${tokenParts.email}login`);
166
+ web.shared.middleware.api.spamPrevention.userLogin().reset(frame.options.ip, `${tokenParts.email}login`);
167
167
  return params;
168
168
  });
169
169
  });
@@ -0,0 +1,19 @@
1
+ const storage = require('../../adapters/storage');
2
+
3
+ module.exports = {
4
+ docName: 'files',
5
+ upload: {
6
+ statusCode: 201,
7
+ permissions: false,
8
+ async query(frame) {
9
+ const filePath = await storage.getStorage('files').save({
10
+ name: frame.file.originalname,
11
+ path: frame.file.path
12
+ });
13
+
14
+ return {
15
+ filePath
16
+ };
17
+ }
18
+ }
19
+ };
@@ -109,6 +109,10 @@ module.exports = {
109
109
  return shared.pipeline(require('./media'), localUtils);
110
110
  },
111
111
 
112
+ get files() {
113
+ return shared.pipeline(require('./files'), localUtils);
114
+ },
115
+
112
116
  get tags() {
113
117
  return shared.pipeline(require('./tags'), localUtils);
114
118
  },
@@ -1,3 +1,4 @@
1
+ const path = require('path');
1
2
  const storage = require('../../adapters/storage');
2
3
 
3
4
  module.exports = {
@@ -6,17 +7,36 @@ module.exports = {
6
7
  statusCode: 201,
7
8
  permissions: false,
8
9
  async query(frame) {
9
- let thumbnail = null;
10
+ let thumbnailPath = null;
10
11
  if (frame.files.thumbnail && frame.files.thumbnail[0]) {
11
- thumbnail = await storage.getStorage('media').save(frame.files.thumbnail[0]);
12
+ thumbnailPath = await storage.getStorage('media').save(frame.files.thumbnail[0]);
12
13
  }
13
14
 
14
- const file = await storage.getStorage('media').save(frame.files.file[0]);
15
+ const filePath = await storage.getStorage('media').save(frame.files.file[0]);
15
16
 
16
17
  return {
17
- filePath: file,
18
- thumbnailPath: thumbnail
18
+ filePath,
19
+ thumbnailPath
19
20
  };
20
21
  }
22
+ },
23
+
24
+ uploadThumbnail: {
25
+ permissions: false,
26
+ options: [
27
+ 'url'
28
+ ],
29
+ async query(frame) {
30
+ const mediaStorage = storage.getStorage('media');
31
+ const targetDir = path.dirname(mediaStorage.urlToPath(frame.data.url));
32
+
33
+ // NOTE: need to cleanup otherwise the parent media name won't match thumb name
34
+ // due to "unique name" generation during save
35
+ if (mediaStorage.exists(frame.file.name, targetDir)) {
36
+ await mediaStorage.delete(frame.file.name, targetDir);
37
+ }
38
+
39
+ return await mediaStorage.save(frame.file, targetDir);
40
+ }
21
41
  }
22
42
  };
@@ -3,6 +3,9 @@ const externalRequest = require('../../lib/request-external');
3
3
 
4
4
  const OEmbed = require('../../services/oembed');
5
5
  const oembed = new OEmbed({config, externalRequest});
6
+ const NFT = require('../../services/nft-oembed');
7
+ const nft = new NFT();
8
+ oembed.registerProvider(nft);
6
9
 
7
10
  module.exports = {
8
11
  docName: 'oembed',
@@ -35,6 +35,10 @@ module.exports = {
35
35
  return require('./members');
36
36
  },
37
37
 
38
+ get media() {
39
+ return require('./media');
40
+ },
41
+
38
42
  get products() {
39
43
  return require('./products');
40
44
  },
@@ -0,0 +1,8 @@
1
+ const path = require('path');
2
+
3
+ module.exports = {
4
+ uploadThumbnail(apiConfig, frame) {
5
+ const parentFileName = path.basename(frame.data.url, path.extname(frame.data.url));
6
+ frame.file.name = `${parentFileName}_thumb${frame.file.ext}`;
7
+ }
8
+ };
@@ -1,25 +1,32 @@
1
1
  const _ = require('lodash');
2
+ const labs = require('../../../../../../shared/labs');
2
3
  const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:config');
3
4
 
4
5
  module.exports = {
5
6
  all(data, apiConfig, frame) {
6
7
  debug('all');
7
8
 
9
+ const keys = [
10
+ 'version',
11
+ 'environment',
12
+ 'database',
13
+ 'mail',
14
+ 'useGravatar',
15
+ 'labs',
16
+ 'clientExtensions',
17
+ 'enableDeveloperExperiments',
18
+ 'stripeDirect',
19
+ 'mailgunIsConfigured',
20
+ 'emailAnalytics',
21
+ 'hostSettings'
22
+ ];
23
+
24
+ if (labs.isSet('gifsCard')) {
25
+ keys.push('tenor');
26
+ }
27
+
8
28
  frame.response = {
9
- config: _.pick(data, [
10
- 'version',
11
- 'environment',
12
- 'database',
13
- 'mail',
14
- 'useGravatar',
15
- 'labs',
16
- 'clientExtensions',
17
- 'enableDeveloperExperiments',
18
- 'stripeDirect',
19
- 'mailgunIsConfigured',
20
- 'emailAnalytics',
21
- 'hostSettings'
22
- ])
29
+ config: _.pick(data, keys)
23
30
  };
24
31
  }
25
32
  };
@@ -0,0 +1,27 @@
1
+ const config = require('../../../../../../shared/config');
2
+ const {STATIC_FILES_URL_PREFIX} = require('@tryghost/constants');
3
+
4
+ function getURL(urlPath) {
5
+ const media = new RegExp('^' + config.getSubdir() + '/' + STATIC_FILES_URL_PREFIX);
6
+ const absolute = media.test(urlPath) ? true : false;
7
+
8
+ if (absolute) {
9
+ // Remove the sub-directory from the URL because ghostConfig will add it back.
10
+ urlPath = urlPath.replace(new RegExp('^' + config.getSubdir()), '');
11
+ const baseUrl = config.getSiteUrl().replace(/\/$/, '');
12
+ urlPath = baseUrl + urlPath;
13
+ }
14
+
15
+ return urlPath;
16
+ }
17
+
18
+ module.exports = {
19
+ upload({filePath}, apiConfig, frame) {
20
+ return frame.response = {
21
+ files: [{
22
+ url: getURL(filePath),
23
+ ref: frame.data.ref || null
24
+ }]
25
+ };
26
+ }
27
+ };
@@ -89,6 +89,10 @@ module.exports = {
89
89
  return require('./media');
90
90
  },
91
91
 
92
+ get files() {
93
+ return require('./files');
94
+ },
95
+
92
96
  get tags() {
93
97
  return require('./tags');
94
98
  },
@@ -24,5 +24,14 @@ module.exports = {
24
24
  ref: frame.data.ref || null
25
25
  }]
26
26
  };
27
+ },
28
+
29
+ uploadThumbnail(path, apiConfig, frame) {
30
+ return frame.response = {
31
+ media: [{
32
+ url: getURL(path),
33
+ ref: frame.data.ref || null
34
+ }]
35
+ };
27
36
  }
28
37
  };
@@ -0,0 +1,7 @@
1
+ const limitService = require('../../../../../services/limits');
2
+
3
+ module.exports = {
4
+ async upload(apiConfig, frame) {
5
+ await limitService.errorIfIsOverLimit('uploads', {currentCount: frame.file.size});
6
+ }
7
+ };
@@ -31,6 +31,10 @@ module.exports = {
31
31
  return require('./media');
32
32
  },
33
33
 
34
+ get files() {
35
+ return require('./files');
36
+ },
37
+
34
38
  get settings() {
35
39
  return require('./settings');
36
40
  },