ghost 4.21.0 → 4.22.3
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.
- package/.eslintrc.js +6 -0
- package/Gruntfile.js +2 -0
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +263 -50
- package/content/themes/casper/default.hbs +12 -3
- package/content/themes/casper/index.hbs +25 -23
- package/content/themes/casper/package.json +91 -2
- package/content/themes/casper/partials/post-card.hbs +1 -1
- package/content/themes/casper/post.hbs +18 -14
- package/content/themes/casper/yarn.lock +245 -192
- package/core/boot.js +8 -0
- package/core/bridge.js +14 -0
- package/core/built/assets/{chunk.3.065ee3c3bdf674bd81a4.js → chunk.3.324fd0cc598c73650219.js} +59 -59
- package/core/built/assets/{ghost-dark-1328db4a7dd128305646305a8731bcfe.css → ghost-dark-39fb496d051565531062d7e047d1c0b1.css} +1 -1
- package/core/built/assets/{ghost.min-5abc69c04ad1d5301a857e01009b9c05.css → ghost.min-4207edfc1ae0a3f9f6505ca00d20b0c0.css} +1 -1
- package/core/built/assets/{ghost.min-6c546c322127ae6d1d1b0ddbf34be75b.js → ghost.min-7da921f6c6cac3fe10da1ba104575440.js} +1775 -1897
- package/core/built/assets/{vendor.min-c6ef90bfd7eff256e10b85583bfe9a74.js → vendor.min-413f887176a041e6dbf88214ca9a7481.js} +6849 -6688
- package/core/frontend/helpers/asset.js +9 -1
- package/core/frontend/helpers/ghost_head.js +13 -1
- package/core/frontend/services/card-assets/index.js +16 -0
- package/core/frontend/services/card-assets/service.js +109 -0
- package/core/frontend/services/theme-engine/config/defaults.json +4 -1
- package/core/frontend/services/theme-engine/config/index.js +1 -1
- package/core/frontend/src/cards/css/bookmark.css +83 -0
- package/core/frontend/src/cards/css/button.css +30 -0
- package/core/frontend/src/cards/css/callout.css +12 -0
- package/core/frontend/src/cards/css/gallery.css +36 -0
- package/core/frontend/src/cards/css/nft.css +85 -0
- package/core/frontend/src/cards/js/gallery.js +8 -0
- package/core/frontend/web/middleware/serve-public-file.js +10 -1
- package/core/frontend/web/routes.js +0 -1
- package/core/frontend/web/site.js +13 -9
- package/core/server/adapters/storage/LocalFilesStorage.js +17 -0
- package/core/server/adapters/storage/LocalImagesStorage.js +51 -0
- package/core/server/adapters/storage/LocalMediaStorage.js +24 -0
- package/core/server/adapters/storage/{LocalFileStorage.js → LocalStorageBase.js} +64 -51
- package/core/server/adapters/storage/index.js +1 -1
- package/core/server/adapters/storage/utils.js +2 -2
- package/core/server/api/canary/files.js +19 -0
- package/core/server/api/canary/index.js +8 -0
- package/core/server/api/canary/media.js +42 -0
- package/core/server/api/canary/oembed.js +3 -0
- package/core/server/api/canary/redirects.js +1 -6
- package/core/server/api/canary/utils/serializers/input/index.js +4 -0
- package/core/server/api/canary/utils/serializers/input/media.js +8 -0
- package/core/server/api/canary/utils/serializers/input/pages.js +8 -0
- package/core/server/api/canary/utils/serializers/output/config.js +21 -14
- package/core/server/api/canary/utils/serializers/output/files.js +27 -0
- package/core/server/api/canary/utils/serializers/output/index.js +8 -0
- package/core/server/api/canary/utils/serializers/output/media.js +37 -0
- package/core/server/api/canary/utils/validators/input/files.js +7 -0
- package/core/server/api/canary/utils/validators/input/index.js +8 -0
- package/core/server/api/canary/utils/validators/input/media.js +11 -0
- package/core/server/api/v2/redirects.js +1 -6
- package/core/server/api/v3/members.js +5 -1
- package/core/server/api/v3/redirects.js +1 -6
- package/core/server/data/migrations/utils.js +55 -16
- package/core/server/data/migrations/versions/4.22/01-add-is-launch-complete-setting.js +8 -0
- package/core/server/data/migrations/versions/4.22/02-update-launch-complete-setting-from-user-data.js +39 -0
- package/core/server/data/schema/default-settings.json +8 -0
- package/core/server/frontend/ghost.min.css +1 -1
- package/core/server/lib/image/blog-icon.js +2 -4
- package/core/server/lib/image/image-size.js +1 -1
- package/core/server/services/limits.js +3 -6
- package/core/server/services/mega/template.js +62 -1
- package/core/server/services/nft-oembed.js +71 -0
- package/core/server/services/oembed.js +145 -110
- package/core/server/services/offers/service.js +1 -31
- package/core/server/services/public-config/config.js +2 -1
- package/core/server/services/redirects/api.js +270 -0
- package/core/server/services/redirects/index.js +27 -12
- package/core/server/services/stripe/index.js +4 -2
- package/core/server/services/themes/ThemeStorage.js +5 -5
- package/core/server/services/url/Resource.js +1 -1
- package/core/server/services/url/Resources.js +28 -21
- package/core/server/services/url/UrlService.js +66 -8
- package/core/server/services/url/Urls.js +7 -2
- package/core/server/services/url/index.js +8 -1
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/canary/admin/routes.js +28 -4
- package/core/server/web/api/middleware/cors.js +7 -7
- package/core/server/web/api/middleware/upload.js +117 -10
- package/core/server/web/members/app.js +1 -1
- package/core/server/web/shared/middlewares/index.js +0 -4
- package/core/shared/config/defaults.json +5 -1
- package/core/shared/config/helpers.js +4 -0
- package/core/shared/config/overrides.json +8 -0
- package/core/shared/labs.js +12 -3
- package/package.json +28 -27
- package/urls.json +597 -0
- package/yarn.lock +972 -941
- package/core/built/assets/img/themes/Editorial-a25a4a34c04dedd858bd5e05ef388b1c.jpg +0 -0
- package/core/built/assets/img/themes/Massively-06edf00108429f7fb8e65f190fba34fe.jpg +0 -0
- package/core/server/services/redirects/settings.js +0 -234
- package/core/server/web/shared/middlewares/custom-redirects.js +0 -128
|
@@ -1,15 +1,30 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
1
|
+
const config = require('../../../shared/config');
|
|
2
|
+
const urlUtils = require('../../../shared/url-utils');
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
getRedirectsFilePath: settings.getRedirectsFilePath,
|
|
12
|
-
get: settings.get,
|
|
13
|
-
setFromFilePath: settings.setFromFilePath
|
|
4
|
+
const DynamicRedirectManager = require('@tryghost/express-dynamic-redirects');
|
|
5
|
+
const CustomRedirectsAPI = require('./api');
|
|
6
|
+
|
|
7
|
+
const redirectManager = new DynamicRedirectManager({
|
|
8
|
+
permanentMaxAge: config.get('caching:customRedirects:maxAge'),
|
|
9
|
+
getSubdirectoryURL: (pathname) => {
|
|
10
|
+
return urlUtils.urlJoin(urlUtils.getSubdir(), pathname);
|
|
14
11
|
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
let customRedirectsAPI;
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
init() {
|
|
18
|
+
customRedirectsAPI = new CustomRedirectsAPI({
|
|
19
|
+
basePath: config.getContentPath('data')
|
|
20
|
+
}, redirectManager);
|
|
21
|
+
|
|
22
|
+
return customRedirectsAPI.init();
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
get api() {
|
|
26
|
+
return customRedirectsAPI;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
middleware: redirectManager.handleRequest
|
|
15
30
|
};
|
|
@@ -27,7 +27,10 @@ function configureApi() {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const debouncedConfigureApi = _.debounce(
|
|
30
|
+
const debouncedConfigureApi = _.debounce(() => {
|
|
31
|
+
configureApi();
|
|
32
|
+
events.emit('services.stripe.reconfigured');
|
|
33
|
+
}, 600);
|
|
31
34
|
|
|
32
35
|
module.exports = {
|
|
33
36
|
async init() {
|
|
@@ -37,7 +40,6 @@ module.exports = {
|
|
|
37
40
|
return;
|
|
38
41
|
}
|
|
39
42
|
debouncedConfigureApi();
|
|
40
|
-
events.emit('services.stripe.reconfigured');
|
|
41
43
|
});
|
|
42
44
|
},
|
|
43
45
|
|
|
@@ -4,16 +4,16 @@ const path = require('path');
|
|
|
4
4
|
const config = require('../../../shared/config');
|
|
5
5
|
const security = require('@tryghost/security');
|
|
6
6
|
const {compress} = require('@tryghost/zip');
|
|
7
|
-
const
|
|
7
|
+
const LocalStorageBase = require('../../adapters/storage/LocalStorageBase');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @TODO: combine with loader.js?
|
|
11
11
|
*/
|
|
12
|
-
class ThemeStorage extends
|
|
12
|
+
class ThemeStorage extends LocalStorageBase {
|
|
13
13
|
constructor() {
|
|
14
|
-
super(
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
super({
|
|
15
|
+
storagePath: config.getContentPath('themes')
|
|
16
|
+
});
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
getTargetDir() {
|
|
@@ -8,7 +8,7 @@ const errors = require('@tryghost/errors');
|
|
|
8
8
|
class Resource extends EventEmitter {
|
|
9
9
|
/**
|
|
10
10
|
* @param {('posts'|'pages'|'tags'|'authors')} type - of the resource
|
|
11
|
-
* @param {Object} obj - object data to
|
|
11
|
+
* @param {Object} obj - object data to store
|
|
12
12
|
*/
|
|
13
13
|
constructor(type, obj) {
|
|
14
14
|
super();
|
|
@@ -43,38 +43,57 @@ class Resources {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* @description
|
|
46
|
+
* @description Initialize the resource config. We currently fetch the data straight via the the model layer,
|
|
47
47
|
* but because Ghost supports multiple API versions, we have to ensure we load the correct data.
|
|
48
48
|
*
|
|
49
49
|
* @TODO: https://github.com/TryGhost/Ghost/issues/10360
|
|
50
|
-
* @private
|
|
51
50
|
*/
|
|
52
|
-
|
|
51
|
+
initResourceConfig() {
|
|
53
52
|
if (!_.isEmpty(this.resourcesConfig)) {
|
|
54
53
|
return;
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
const bridge = require('../../../bridge');
|
|
58
|
-
|
|
59
|
-
this.resourcesConfig = require(`./configs/${
|
|
57
|
+
const resourcesAPIVersion = bridge.getFrontendApiVersion();
|
|
58
|
+
this.resourcesConfig = require(`./configs/${resourcesAPIVersion}`);
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
/**
|
|
63
|
-
* @description Helper function to
|
|
64
|
-
* events to get notified about updates/deletions/inserts.
|
|
62
|
+
* @description Helper function to initialize data fetching.
|
|
65
63
|
*/
|
|
66
64
|
fetchResources() {
|
|
67
65
|
const ops = [];
|
|
68
66
|
debug('fetchResources');
|
|
69
67
|
|
|
70
|
-
this._initResourceConfig();
|
|
71
|
-
|
|
72
68
|
// NOTE: Iterate over all resource types (posts, users etc..) and call `_fetch`.
|
|
73
69
|
_.each(this.resourcesConfig, (resourceConfig) => {
|
|
74
70
|
this.data[resourceConfig.type] = [];
|
|
75
71
|
|
|
76
72
|
// NOTE: We are querying knex directly, because the Bookshelf ORM overhead is too slow.
|
|
77
73
|
ops.push(this._fetch(resourceConfig));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return Promise.all(ops);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @description Each resource type needs to register resource/model events to get notified
|
|
81
|
+
* about updates/deletions/inserts.
|
|
82
|
+
*
|
|
83
|
+
* For example for a "tag" resource type with following configuration:
|
|
84
|
+
* events: {
|
|
85
|
+
* add: 'tag.added',
|
|
86
|
+
* update: ['tag.edited', 'tag.attached', 'tag.detached'],
|
|
87
|
+
* remove: 'tag.deleted'
|
|
88
|
+
* }
|
|
89
|
+
* there would be:
|
|
90
|
+
* 1 event listener connected to "_onResourceAdded" handler and it's 'tag.added' event
|
|
91
|
+
* 3 event listeners connected to "_onResourceUpdated" handler and it's 'tag.edited', 'tag.attached', 'tag.detached' events
|
|
92
|
+
* 1 event listener connected to "_onResourceRemoved" handler and it's 'tag.deleted' event
|
|
93
|
+
*/
|
|
94
|
+
initEvenListeners() {
|
|
95
|
+
_.each(this.resourcesConfig, (resourceConfig) => {
|
|
96
|
+
this.data[resourceConfig.type] = [];
|
|
78
97
|
|
|
79
98
|
this._listenOn(resourceConfig.events.add, (model) => {
|
|
80
99
|
return this._onResourceAdded.bind(this)(resourceConfig.type, model);
|
|
@@ -96,16 +115,6 @@ class Resources {
|
|
|
96
115
|
return this._onResourceRemoved.bind(this)(resourceConfig.type, model);
|
|
97
116
|
});
|
|
98
117
|
});
|
|
99
|
-
|
|
100
|
-
Promise.all(ops)
|
|
101
|
-
.then(() => {
|
|
102
|
-
// CASE: all resources are fetched, start the queue
|
|
103
|
-
this.queue.start({
|
|
104
|
-
event: 'init',
|
|
105
|
-
tolerance: 100,
|
|
106
|
-
requiredSubscriberCount: 1
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
/**
|
|
@@ -430,8 +439,6 @@ class Resources {
|
|
|
430
439
|
* @description Reset this class instance.
|
|
431
440
|
*
|
|
432
441
|
* Is triggered if you switch API versions.
|
|
433
|
-
*
|
|
434
|
-
* @param {Object} options
|
|
435
442
|
*/
|
|
436
443
|
reset() {
|
|
437
444
|
_.each(this.listeners, (obj) => {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
1
2
|
const _debug = require('@tryghost/debug')._base;
|
|
2
3
|
const debug = _debug('ghost:services:url:service');
|
|
3
4
|
const _ = require('lodash');
|
|
4
5
|
const errors = require('@tryghost/errors');
|
|
6
|
+
const labs = require('../../../shared/labs');
|
|
5
7
|
const UrlGenerator = require('./UrlGenerator');
|
|
6
8
|
const Queue = require('./Queue');
|
|
7
9
|
const Urls = require('./Urls');
|
|
@@ -17,12 +19,18 @@ const events = require('../../lib/common/events');
|
|
|
17
19
|
* It will tell you if the url generation is in progress or not.
|
|
18
20
|
*/
|
|
19
21
|
class UrlService {
|
|
20
|
-
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} options
|
|
25
|
+
* @param {String} [options.urlCachePath] - path to store cached URLs at
|
|
26
|
+
*/
|
|
27
|
+
constructor({urlCachePath} = {}) {
|
|
21
28
|
this.utils = urlUtils;
|
|
22
|
-
|
|
29
|
+
this.urlCachePath = urlCachePath;
|
|
23
30
|
this.finished = false;
|
|
24
31
|
this.urlGenerators = [];
|
|
25
32
|
|
|
33
|
+
// Get urls
|
|
26
34
|
this.urls = new Urls();
|
|
27
35
|
this.queue = new Queue();
|
|
28
36
|
this.resources = new Resources(this.queue);
|
|
@@ -281,13 +289,63 @@ class UrlService {
|
|
|
281
289
|
}
|
|
282
290
|
|
|
283
291
|
/**
|
|
284
|
-
* @description
|
|
285
|
-
*
|
|
286
|
-
* @TODO: Either remove this helper or rename to `_init`, because it's a little confusing,
|
|
287
|
-
* because this service get's initalised via events.
|
|
292
|
+
* @description Initializes components needed for the URL Service to function
|
|
288
293
|
*/
|
|
289
|
-
init() {
|
|
290
|
-
this.resources.
|
|
294
|
+
async init() {
|
|
295
|
+
this.resources.initResourceConfig();
|
|
296
|
+
this.resources.initEvenListeners();
|
|
297
|
+
|
|
298
|
+
const persistedUrls = await this.fetchUrls();
|
|
299
|
+
if (persistedUrls) {
|
|
300
|
+
this.urls = new Urls({
|
|
301
|
+
urls: persistedUrls
|
|
302
|
+
});
|
|
303
|
+
this.finished = true;
|
|
304
|
+
} else {
|
|
305
|
+
await this.resources.fetchResources();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// CASE: all resources are fetched, start the queue
|
|
309
|
+
this.queue.start({
|
|
310
|
+
event: 'init',
|
|
311
|
+
tolerance: 100,
|
|
312
|
+
requiredSubscriberCount: 1
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async persistUrls() {
|
|
317
|
+
if (!labs.isSet('urlCache') || !this.urlCachePath) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return fs.writeFile(this.urlCachePath, JSON.stringify(this.urls.urls, null, 4));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async fetchUrls() {
|
|
325
|
+
if (!labs.isSet('urlCache') || !this.urlCachePath) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let urlsCacheExists = false;
|
|
330
|
+
let urls;
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
await fs.stat(this.urlCachePath);
|
|
334
|
+
urlsCacheExists = true;
|
|
335
|
+
} catch (e) {
|
|
336
|
+
urlsCacheExists = false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (urlsCacheExists) {
|
|
340
|
+
try {
|
|
341
|
+
const urlsFile = await fs.readFile(this.urlCachePath, 'utf8');
|
|
342
|
+
urls = JSON.parse(urlsFile);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
//noop as we'd start a long boot process if there are any errors in the file
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return urls;
|
|
291
349
|
}
|
|
292
350
|
|
|
293
351
|
/**
|
|
@@ -20,8 +20,13 @@ const events = require('../../lib/common/events');
|
|
|
20
20
|
* You can easily ask `this.urls[resourceId]`.
|
|
21
21
|
*/
|
|
22
22
|
class Urls {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} [options]
|
|
26
|
+
* @param {Object} [options.urls] map of available URLs with their resources
|
|
27
|
+
*/
|
|
28
|
+
constructor({urls = {}} = {}) {
|
|
29
|
+
this.urls = urls;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/**
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const config = require('../../../shared/config');
|
|
1
3
|
const UrlService = require('./UrlService');
|
|
2
|
-
|
|
4
|
+
|
|
5
|
+
// NOTE: instead of a path we could give UrlService a "data-resolver" of some sort
|
|
6
|
+
// so it doesn't have to contain the logic to read data at all. This would be
|
|
7
|
+
// a possible improvement in the future
|
|
8
|
+
const urlCachePath = path.join(config.getContentPath('data'), 'urls.json');
|
|
9
|
+
const urlService = new UrlService({urlCachePath});
|
|
3
10
|
|
|
4
11
|
// Singleton
|
|
5
12
|
module.exports = urlService;
|
|
@@ -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%2F%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%224.
|
|
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%2F%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%224.22%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22emberKeyboard%22%3A%7B%22disableInputsInitializer%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
|
|
12
12
|
|
|
13
13
|
<meta name="HandheldFriendly" content="True" />
|
|
14
14
|
<meta name="MobileOptimized" content="320" />
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
<link rel="stylesheet" href="assets/vendor.min-987af30228885bce50f05c4723fe6f53.css">
|
|
44
|
-
<link rel="stylesheet" href="assets/ghost.min-
|
|
44
|
+
<link rel="stylesheet" href="assets/ghost.min-4207edfc1ae0a3f9f6505ca00d20b0c0.css" title="light">
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
<div id="ember-basic-dropdown-wormhole"></div>
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
<script src="assets/vendor.min-
|
|
63
|
-
<script src="assets/ghost.min-
|
|
62
|
+
<script src="assets/vendor.min-413f887176a041e6dbf88214ca9a7481.js"></script>
|
|
63
|
+
<script src="assets/ghost.min-7da921f6c6cac3fe10da1ba104575440.js"></script>
|
|
64
64
|
|
|
65
65
|
</body>
|
|
66
66
|
</html>
|
|
@@ -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%2F%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%224.
|
|
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%2F%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%224.22%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22emberKeyboard%22%3A%7B%22disableInputsInitializer%22%3Atrue%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
|
|
12
12
|
|
|
13
13
|
<meta name="HandheldFriendly" content="True" />
|
|
14
14
|
<meta name="MobileOptimized" content="320" />
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
<link rel="stylesheet" href="assets/vendor.min-987af30228885bce50f05c4723fe6f53.css">
|
|
44
|
-
<link rel="stylesheet" href="assets/ghost.min-
|
|
44
|
+
<link rel="stylesheet" href="assets/ghost.min-4207edfc1ae0a3f9f6505ca00d20b0c0.css" title="light">
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
<div id="ember-basic-dropdown-wormhole"></div>
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
<script src="assets/vendor.min-
|
|
63
|
-
<script src="assets/ghost.min-
|
|
62
|
+
<script src="assets/vendor.min-413f887176a041e6dbf88214ca9a7481.js"></script>
|
|
63
|
+
<script src="assets/ghost.min-7da921f6c6cac3fe10da1ba104575440.js"></script>
|
|
64
64
|
|
|
65
65
|
</body>
|
|
66
66
|
</html>
|
|
@@ -100,10 +100,10 @@ module.exports = function apiRoutes() {
|
|
|
100
100
|
router.del('/members', mw.authAdminApi, http(api.members.bulkDestroy));
|
|
101
101
|
router.put('/members/bulk', mw.authAdminApi, http(api.members.bulkEdit));
|
|
102
102
|
|
|
103
|
-
router.get('/offers',
|
|
104
|
-
router.post('/offers',
|
|
105
|
-
router.get('/offers/:id',
|
|
106
|
-
router.put('/offers/:id',
|
|
103
|
+
router.get('/offers', mw.authAdminApi, http(api.offers.browse));
|
|
104
|
+
router.post('/offers', mw.authAdminApi, http(api.offers.add));
|
|
105
|
+
router.get('/offers/:id', mw.authAdminApi, http(api.offers.read));
|
|
106
|
+
router.put('/offers/:id', mw.authAdminApi, http(api.offers.edit));
|
|
107
107
|
|
|
108
108
|
router.get('/members/stats/count', mw.authAdminApi, http(api.members.memberStats));
|
|
109
109
|
router.get('/members/stats/mrr', mw.authAdminApi, http(api.members.mrrStats));
|
|
@@ -236,6 +236,30 @@ module.exports = function apiRoutes() {
|
|
|
236
236
|
http(api.images.upload)
|
|
237
237
|
);
|
|
238
238
|
|
|
239
|
+
// ## media
|
|
240
|
+
router.post('/media/upload',
|
|
241
|
+
labs.enabledMiddleware('mediaAPI'),
|
|
242
|
+
mw.authAdminApi,
|
|
243
|
+
apiMw.upload.media('file', 'thumbnail'),
|
|
244
|
+
apiMw.upload.mediaValidation({type: 'media'}),
|
|
245
|
+
http(api.media.upload)
|
|
246
|
+
);
|
|
247
|
+
router.put('/media/thumbnail/upload',
|
|
248
|
+
labs.enabledMiddleware('mediaAPI'),
|
|
249
|
+
mw.authAdminApi,
|
|
250
|
+
apiMw.upload.single('file'),
|
|
251
|
+
apiMw.upload.validation({type: 'images'}),
|
|
252
|
+
http(api.media.uploadThumbnail)
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// ## files
|
|
256
|
+
router.post('/files/upload',
|
|
257
|
+
labs.enabledMiddleware('filesAPI'),
|
|
258
|
+
mw.authAdminApi,
|
|
259
|
+
apiMw.upload.single('file'),
|
|
260
|
+
http(api.files.upload)
|
|
261
|
+
);
|
|
262
|
+
|
|
239
263
|
// ## Invites
|
|
240
264
|
router.get('/invites', mw.authAdminApi, http(api.invites.browse));
|
|
241
265
|
router.get('/invites/:id', mw.authAdminApi, http(api.invites.read));
|
|
@@ -3,7 +3,7 @@ const url = require('url');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const urlUtils = require('../../../../shared/url-utils');
|
|
5
5
|
|
|
6
|
-
let
|
|
6
|
+
let allowlist = [];
|
|
7
7
|
const ENABLE_CORS = {origin: true, maxAge: 86400};
|
|
8
8
|
const DISABLE_CORS = {origin: false};
|
|
9
9
|
|
|
@@ -46,16 +46,16 @@ function getUrls() {
|
|
|
46
46
|
return urls;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function
|
|
49
|
+
function getAllowlist() {
|
|
50
50
|
// This needs doing just one time after init
|
|
51
|
-
if (
|
|
51
|
+
if (allowlist.length === 0) {
|
|
52
52
|
// origins that always match: localhost, local IPs, etc.
|
|
53
|
-
|
|
53
|
+
allowlist = allowlist.concat(getIPs());
|
|
54
54
|
// Trusted urls from config.js
|
|
55
|
-
|
|
55
|
+
allowlist = allowlist.concat(getUrls());
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
return
|
|
58
|
+
return allowlist;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
@@ -73,7 +73,7 @@ function handleCORS(req, cb) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// Origin matches whitelist
|
|
76
|
-
if (
|
|
76
|
+
if (getAllowlist().indexOf(url.parse(origin).hostname) > -1) {
|
|
77
77
|
return cb(null, ENABLE_CORS);
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -31,23 +31,30 @@ const messages = {
|
|
|
31
31
|
icons: {
|
|
32
32
|
missingFile: 'Please select an icon.',
|
|
33
33
|
invalidFile: 'Icon must be a square .ico or .png file between 60px – 1,000px, under 100kb.'
|
|
34
|
+
},
|
|
35
|
+
media: {
|
|
36
|
+
missingFile: 'Please select a media file.',
|
|
37
|
+
invalidFile: 'Please select a valid media file.'
|
|
38
|
+
},
|
|
39
|
+
thumbnail: {
|
|
40
|
+
missingFile: 'Please select a thumbnail.',
|
|
41
|
+
invalidFile: 'Please select a valid thumbnail.'
|
|
34
42
|
}
|
|
35
43
|
};
|
|
36
44
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
multer: multer({dest: os.tmpdir()})
|
|
40
|
-
};
|
|
45
|
+
const enabledClear = config.get('uploadClear') || true;
|
|
46
|
+
const upload = multer({dest: os.tmpdir()});
|
|
41
47
|
|
|
42
48
|
const deleteSingleFile = file => fs.unlink(file.path).catch(err => logging.error(err));
|
|
43
49
|
|
|
44
50
|
const single = name => (req, res, next) => {
|
|
45
|
-
const singleUpload = upload.
|
|
51
|
+
const singleUpload = upload.single(name);
|
|
52
|
+
|
|
46
53
|
singleUpload(req, res, (err) => {
|
|
47
54
|
if (err) {
|
|
48
55
|
return next(err);
|
|
49
56
|
}
|
|
50
|
-
if (
|
|
57
|
+
if (enabledClear) {
|
|
51
58
|
const deleteFiles = () => {
|
|
52
59
|
res.removeListener('finish', deleteFiles);
|
|
53
60
|
res.removeListener('close', deleteFiles);
|
|
@@ -70,6 +77,43 @@ const single = name => (req, res, next) => {
|
|
|
70
77
|
});
|
|
71
78
|
};
|
|
72
79
|
|
|
80
|
+
const media = (fileName, thumbName) => (req, res, next) => {
|
|
81
|
+
const mediaUpload = upload.fields([{
|
|
82
|
+
name: fileName,
|
|
83
|
+
maxCount: 1
|
|
84
|
+
}, {
|
|
85
|
+
name: thumbName,
|
|
86
|
+
maxCount: 1
|
|
87
|
+
}]);
|
|
88
|
+
|
|
89
|
+
mediaUpload(req, res, (err) => {
|
|
90
|
+
if (err) {
|
|
91
|
+
return next(err);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (enabledClear) {
|
|
95
|
+
const deleteFiles = () => {
|
|
96
|
+
res.removeListener('finish', deleteFiles);
|
|
97
|
+
res.removeListener('close', deleteFiles);
|
|
98
|
+
if (!req.disableUploadClear) {
|
|
99
|
+
if (req.files.file) {
|
|
100
|
+
return req.files.file.forEach(deleteSingleFile);
|
|
101
|
+
}
|
|
102
|
+
if (req.files.thumbnail) {
|
|
103
|
+
return req.files.thumbnail.forEach(deleteSingleFile);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
if (!req.disableUploadClear) {
|
|
108
|
+
res.on('finish', deleteFiles);
|
|
109
|
+
res.on('close', deleteFiles);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
next();
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
73
117
|
const checkFileExists = (fileData) => {
|
|
74
118
|
return !!(fileData.mimetype && fileData.path);
|
|
75
119
|
};
|
|
@@ -84,9 +128,13 @@ const checkFileIsValid = (fileData, types, extensions) => {
|
|
|
84
128
|
return false;
|
|
85
129
|
};
|
|
86
130
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
131
|
+
/**
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} options
|
|
134
|
+
* @param {String} options.type - type of the file
|
|
135
|
+
* @returns {Function}
|
|
136
|
+
*/
|
|
137
|
+
const validation = function ({type}) {
|
|
90
138
|
// if we finish the data/importer logic, we forward the request to the specified importer
|
|
91
139
|
return function uploadValidation(req, res, next) {
|
|
92
140
|
const extensions = (config.get('uploads')[type] && config.get('uploads')[type].extensions) || [];
|
|
@@ -116,9 +164,68 @@ const validation = function (options) {
|
|
|
116
164
|
};
|
|
117
165
|
};
|
|
118
166
|
|
|
167
|
+
/**
|
|
168
|
+
*
|
|
169
|
+
* @param {Object} options
|
|
170
|
+
* @param {String} options.type - type of the file
|
|
171
|
+
* @returns {Function}
|
|
172
|
+
*/
|
|
173
|
+
const mediaValidation = function ({type}) {
|
|
174
|
+
return function mediaUploadValidation(req, res, next) {
|
|
175
|
+
const extensions = (config.get('uploads')[type] && config.get('uploads')[type].extensions) || [];
|
|
176
|
+
const contentTypes = (config.get('uploads')[type] && config.get('uploads')[type].contentTypes) || [];
|
|
177
|
+
|
|
178
|
+
const thumbnailExtensions = (config.get('uploads').thumbnails && config.get('uploads').thumbnails.extensions) || [];
|
|
179
|
+
const thumbnailContentTypes = (config.get('uploads').thumbnails && config.get('uploads').thumbnails.contentTypes) || [];
|
|
180
|
+
|
|
181
|
+
const {file: [file] = []} = req.files;
|
|
182
|
+
if (!file || !checkFileExists(file)) {
|
|
183
|
+
return next(new errors.ValidationError({
|
|
184
|
+
message: tpl(messages[type].missingFile)
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
req.file = file;
|
|
189
|
+
req.file.name = req.file.originalname;
|
|
190
|
+
req.file.type = req.file.mimetype;
|
|
191
|
+
req.file.ext = path.extname(req.file.name).toLowerCase();
|
|
192
|
+
|
|
193
|
+
if (!checkFileIsValid(req.file, contentTypes, extensions)) {
|
|
194
|
+
return next(new errors.UnsupportedMediaTypeError({
|
|
195
|
+
message: tpl(messages[type].invalidFile, {extensions: extensions})
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const {thumbnail: [thumbnailFile] = []} = req.files;
|
|
200
|
+
|
|
201
|
+
if (thumbnailFile) {
|
|
202
|
+
if (!checkFileExists(thumbnailFile)) {
|
|
203
|
+
return next(new errors.ValidationError({
|
|
204
|
+
message: tpl(messages.thumbnail.missingFile)
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
req.thumbnail = thumbnailFile;
|
|
209
|
+
req.thumbnail.ext = path.extname(thumbnailFile.originalname).toLowerCase();
|
|
210
|
+
req.thumbnail.name = `${path.basename(req.file.name, path.extname(req.file.name))}_thumb${req.thumbnail.ext}`;
|
|
211
|
+
req.thumbnail.type = req.thumbnail.mimetype;
|
|
212
|
+
|
|
213
|
+
if (!checkFileIsValid(req.thumbnail, thumbnailContentTypes, thumbnailExtensions)) {
|
|
214
|
+
return next(new errors.UnsupportedMediaTypeError({
|
|
215
|
+
message: tpl(messages.thumbnail.invalidFile, {extensions: thumbnailExtensions})
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
next();
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
119
224
|
module.exports = {
|
|
120
225
|
single,
|
|
121
|
-
|
|
226
|
+
media,
|
|
227
|
+
validation,
|
|
228
|
+
mediaValidation
|
|
122
229
|
};
|
|
123
230
|
|
|
124
231
|
// Exports for testing only
|
|
@@ -37,7 +37,7 @@ module.exports = function setupMembersApp() {
|
|
|
37
37
|
membersApp.put('/api/member', bodyParser.json({limit: '1mb'}), middleware.updateMemberData);
|
|
38
38
|
membersApp.post('/api/member/email', bodyParser.json({limit: '1mb'}), (req, res) => membersService.api.middleware.updateEmailAddress(req, res));
|
|
39
39
|
membersApp.get('/api/session', middleware.getIdentityToken);
|
|
40
|
-
membersApp.get('/api/offers/:id',
|
|
40
|
+
membersApp.get('/api/offers/:id', middleware.getOfferData);
|
|
41
41
|
membersApp.delete('/api/session', middleware.deleteSession);
|
|
42
42
|
membersApp.get('/api/site', middleware.getMemberSiteData);
|
|
43
43
|
|