ghost 5.0.2 → 5.2.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.
- 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 +303 -35
- package/content/themes/casper/default.hbs +17 -7
- package/content/themes/casper/package.json +3 -2
- package/content/themes/casper/partials/icons/facebook.hbs +1 -1
- package/content/themes/casper/partials/icons/fire.hbs +3 -0
- package/content/themes/casper/partials/icons/lock.hbs +5 -0
- package/content/themes/casper/partials/icons/twitter.hbs +1 -1
- package/content/themes/casper/partials/post-card.hbs +48 -19
- package/content/themes/casper/post.hbs +20 -9
- package/core/built/assets/{chunk.3.52b444495dfcf50afb0b.js → chunk.3.dc389a0f93cb5fabd695.js} +19 -19
- package/core/built/assets/{ghost-dark-b6e3268bcae976a8675b0d9ae54540f2.css → ghost-dark-728b7c86791572db8e9126a77b2b7984.css} +1 -1
- package/core/built/assets/{ghost.min-b37a2752b0abda202c14c948b8bc3587.js → ghost.min-0643f338c882395ac93439fda1a36df8.js} +224 -223
- package/core/built/assets/{ghost.min-a40c7b301c702bd84c2fae19366ab5d7.css → ghost.min-f108dfe5d3861a2e1f0d704624261783.css} +1 -1
- package/core/built/assets/img/themes/Casper-19b7267aac5acc6abfaaed7e41eddae8.jpg +0 -0
- package/core/built/assets/{vendor.min-2bca41946dea27dc292428db605d466d.js → vendor.min-ea369e6487643585f35409d474b06789.js} +77 -94
- package/core/frontend/helpers/tiers.js +1 -16
- package/core/frontend/services/rendering/context.js +0 -3
- package/core/frontend/services/rendering/format-response.js +5 -1
- package/core/frontend/services/routing/controllers/static.js +4 -1
- package/core/frontend/services/routing/controllers/unsubscribe.js +10 -34
- package/core/frontend/web/middleware/handle-image-sizes.js +50 -10
- package/core/frontend/web/middleware/serve-favicon.js +3 -18
- package/core/server/api/canary/utils/serializers/output/mappers/posts.js +1 -4
- package/core/server/api/canary/utils/serializers/output/members.js +11 -14
- package/core/server/api/canary/utils/serializers/output/settings.js +13 -0
- package/core/server/api/canary/utils/validators/input/images.js +32 -18
- package/core/server/api/canary/utils/validators/input/settings.js +6 -0
- package/core/server/data/db/connection.js +13 -0
- package/core/server/lib/image/blog-icon.js +51 -27
- package/core/server/lib/mobiledoc.js +1 -1
- package/core/server/models/base/bookshelf.js +2 -0
- package/core/server/models/base/plugins/data-manipulation.js +1 -0
- package/core/server/models/base/plugins/relations.js +30 -0
- package/core/server/models/member.js +0 -12
- package/core/server/models/post.js +7 -4
- package/core/server/models/relations/authors.js +0 -19
- package/core/server/models/user.js +31 -3
- package/core/server/services/bulk-email/bulk-email-processor.js +1 -1
- package/core/server/services/invites/invites.js +1 -0
- package/core/server/services/mega/email-preview.js +1 -1
- package/core/server/services/mega/mega.js +7 -61
- package/core/server/services/members/utils.js +1 -3
- package/core/server/services/webhooks/webhooks-service.js +1 -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/app.js +2 -2
- package/core/server/web/api/canary/content/app.js +1 -1
- package/core/server/web/api/middleware/normalize-image.js +1 -1
- package/core/server/web/members/app.js +3 -3
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/config/overrides.json +5 -2
- package/core/shared/config/utils.js +0 -9
- package/core/shared/labs.js +2 -13
- package/package.json +53 -53
- package/yarn.lock +573 -600
- package/core/built/assets/img/themes/Casper-c7e784d7188cc5d7f097d9b6c97b0263.jpg +0 -0
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Returns a string of the tiers with access to the post.
|
|
5
5
|
// By default, tiers are separated by commas.
|
|
6
|
-
const {labs} = require('../services/proxy');
|
|
7
6
|
const {SafeString, escapeExpression} = require('../services/handlebars');
|
|
8
7
|
|
|
9
8
|
const isString = require('lodash/isString');
|
|
10
9
|
|
|
11
|
-
function tiers(options = {}) {
|
|
10
|
+
module.exports = function tiers(options = {}) {
|
|
12
11
|
options = options || {};
|
|
13
12
|
options.hash = options.hash || {};
|
|
14
13
|
|
|
@@ -42,18 +41,4 @@ function tiers(options = {}) {
|
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
return new SafeString(output);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
module.exports = function productsLabsWrapper() {
|
|
48
|
-
let self = this;
|
|
49
|
-
let args = arguments;
|
|
50
|
-
|
|
51
|
-
return labs.enabledHelper({
|
|
52
|
-
flagKey: 'multipleProducts',
|
|
53
|
-
flagName: 'Tiers',
|
|
54
|
-
helperName: 'tiers',
|
|
55
|
-
helpUrl: 'https://ghost.org/docs/themes/'
|
|
56
|
-
}, () => {
|
|
57
|
-
return tiers.apply(self, args);
|
|
58
|
-
});
|
|
59
44
|
};
|
|
@@ -57,9 +57,6 @@ function setResponseContext(req, res, data) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// @TODO: remove first if condition when only page key is returned
|
|
61
|
-
// ref.: https://github.com/TryGhost/Ghost/issues/10042
|
|
62
|
-
// The first if is now used by the preview route
|
|
63
60
|
if (data && data.page) {
|
|
64
61
|
if (!res.locals.context.includes('page')) {
|
|
65
62
|
res.locals.context.push('page');
|
|
@@ -6,7 +6,7 @@ const {prepareContextResource} = require('../proxy');
|
|
|
6
6
|
*
|
|
7
7
|
* @return {Object} containing page variables
|
|
8
8
|
*/
|
|
9
|
-
function formatPageResponse(result) {
|
|
9
|
+
function formatPageResponse(result, pageAsPost = false) {
|
|
10
10
|
const response = {};
|
|
11
11
|
|
|
12
12
|
if (result.posts) {
|
|
@@ -32,6 +32,10 @@ function formatPageResponse(result) {
|
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
if (pageAsPost && response.page) {
|
|
36
|
+
response.post = response.page;
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
return response;
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -60,7 +60,10 @@ module.exports = function staticController(req, res, next) {
|
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
// This flag solves the confusion about whether the output contains a post or page object by duplicating the objects
|
|
64
|
+
// This is not ideal, but will solve some long standing pain points with dynamic routing until we can overhaul it
|
|
65
|
+
const duplicatePagesAsPosts = true;
|
|
66
|
+
renderer.renderer(req, res, renderer.formatResponse.entries(response, duplicatePagesAsPosts));
|
|
64
67
|
})
|
|
65
68
|
.catch(renderer.handleError(next));
|
|
66
69
|
};
|
|
@@ -1,48 +1,24 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('services:routing:controllers:unsubscribe');
|
|
2
|
-
const path = require('path');
|
|
3
2
|
const url = require('url');
|
|
4
3
|
|
|
5
4
|
const urlUtils = require('../../../../shared/url-utils');
|
|
6
|
-
const megaService = require('../../../../server/services/mega');
|
|
7
|
-
const renderer = require('../../rendering');
|
|
8
|
-
const labs = require('../../../../shared/labs');
|
|
9
5
|
|
|
10
6
|
module.exports = async function unsubscribeController(req, res) {
|
|
11
7
|
debug('unsubscribeController');
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
const {query} = url.parse(req.url, true);
|
|
9
|
+
const {query} = url.parse(req.url, true);
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const redirectUrl = new URL(urlUtils.urlFor('home', true));
|
|
22
|
-
redirectUrl.searchParams.append('uuid', query.uuid);
|
|
23
|
-
if (query.newsletter) {
|
|
24
|
-
redirectUrl.searchParams.append('newsletter', query.newsletter);
|
|
25
|
-
}
|
|
26
|
-
redirectUrl.searchParams.append('action', 'unsubscribe');
|
|
27
|
-
|
|
28
|
-
return res.redirect(302, redirectUrl.href);
|
|
11
|
+
if (!query || !query.uuid) {
|
|
12
|
+
res.writeHead(400);
|
|
13
|
+
return res.end('Email address not found.');
|
|
29
14
|
}
|
|
30
15
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} catch (err) {
|
|
36
|
-
data.error = err.message;
|
|
16
|
+
const redirectUrl = new URL(urlUtils.urlFor('home', true));
|
|
17
|
+
redirectUrl.searchParams.append('uuid', query.uuid);
|
|
18
|
+
if (query.newsletter) {
|
|
19
|
+
redirectUrl.searchParams.append('newsletter', query.newsletter);
|
|
37
20
|
}
|
|
21
|
+
redirectUrl.searchParams.append('action', 'unsubscribe');
|
|
38
22
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
res.routerOptions = {
|
|
42
|
-
type: 'custom',
|
|
43
|
-
templates: templateName,
|
|
44
|
-
defaultTemplate: path.resolve(__dirname, '../../../views/', templateName)
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
return renderer.renderer(req, res, data);
|
|
23
|
+
return res.redirect(302, redirectUrl.href);
|
|
48
24
|
};
|
|
@@ -7,6 +7,8 @@ const activeTheme = require('../../services/theme-engine/active');
|
|
|
7
7
|
const config = require('../../../shared/config');
|
|
8
8
|
|
|
9
9
|
const SIZE_PATH_REGEX = /^\/size\/([^/]+)\//;
|
|
10
|
+
const FORMAT_PATH_REGEX = /^\/format\/([^./]+)\//;
|
|
11
|
+
|
|
10
12
|
const TRAILING_SLASH_REGEX = /\/+$/;
|
|
11
13
|
|
|
12
14
|
module.exports = function (req, res, next) {
|
|
@@ -18,9 +20,29 @@ module.exports = function (req, res, next) {
|
|
|
18
20
|
return next();
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
const
|
|
23
|
+
const requestedDimension = req.url.match(SIZE_PATH_REGEX)[1];
|
|
24
|
+
|
|
25
|
+
// Note that we don't use sizeImageDir because we need to keep the trailing slash
|
|
26
|
+
let imagePath = req.url.replace(`/size/${requestedDimension}`, '');
|
|
27
|
+
|
|
28
|
+
// Check if we want to format the image
|
|
29
|
+
let format = null;
|
|
30
|
+
const matchedFormat = imagePath.match(FORMAT_PATH_REGEX);
|
|
31
|
+
if (matchedFormat) {
|
|
32
|
+
format = matchedFormat[1];
|
|
33
|
+
|
|
34
|
+
// Note that we don't use matchedFormat[0] because we need to keep the trailing slash
|
|
35
|
+
imagePath = imagePath.replace(`/format/${format}`, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
const redirectToOriginal = () => {
|
|
23
|
-
|
|
39
|
+
// We need to keep the first slash here
|
|
40
|
+
let url = req.originalUrl
|
|
41
|
+
.replace(`/size/${requestedDimension}`, '');
|
|
42
|
+
|
|
43
|
+
if (format) {
|
|
44
|
+
url = url.replace(`/format/${format}`, '');
|
|
45
|
+
}
|
|
24
46
|
return res.redirect(url);
|
|
25
47
|
};
|
|
26
48
|
|
|
@@ -31,14 +53,10 @@ module.exports = function (req, res, next) {
|
|
|
31
53
|
return next();
|
|
32
54
|
}
|
|
33
55
|
|
|
34
|
-
// CASE: image transform is not capable of transforming file (e.g. .gif)
|
|
35
|
-
if (!imageTransform.canTransformFileExtension(requestUrlFileExtension)) {
|
|
36
|
-
return redirectToOriginal();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
56
|
const contentImageSizes = config.get('imageOptimization:contentImageSizes');
|
|
57
|
+
const internalImageSizes = config.get('imageOptimization:internalImageSizes');
|
|
40
58
|
const themeImageSizes = activeTheme.get().config('image_sizes');
|
|
41
|
-
const imageSizes = _.merge({}, themeImageSizes, contentImageSizes);
|
|
59
|
+
const imageSizes = _.merge({}, themeImageSizes, internalImageSizes, contentImageSizes);
|
|
42
60
|
|
|
43
61
|
// CASE: no image_sizes config (NOTE - unlikely to be reachable now we have content sizes)
|
|
44
62
|
if (!imageSizes) {
|
|
@@ -63,6 +81,25 @@ module.exports = function (req, res, next) {
|
|
|
63
81
|
return redirectToOriginal();
|
|
64
82
|
}
|
|
65
83
|
|
|
84
|
+
// CASE: image transform is not capable of transforming some files (e.g. .ico)
|
|
85
|
+
if (!imageTransform.canTransformFileExtension(requestUrlFileExtension)) {
|
|
86
|
+
return redirectToOriginal();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (format) {
|
|
90
|
+
// CASE: When formatting, we need to check if the imageTransform package supports this specific format
|
|
91
|
+
if (!imageTransform.canTransformToFormat(format)) {
|
|
92
|
+
// transform not supported
|
|
93
|
+
return redirectToOriginal();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// CASE: when transforming is supported, we need to check if it is desired
|
|
98
|
+
// (e.g. it is not desired to resize SVGs when not formatting them to a different type)
|
|
99
|
+
if (!format && !imageTransform.shouldResizeFileExtension(requestUrlFileExtension)) {
|
|
100
|
+
return redirectToOriginal();
|
|
101
|
+
}
|
|
102
|
+
|
|
66
103
|
const storageInstance = storage.getStorage('images');
|
|
67
104
|
// CASE: unsupported storage adapter
|
|
68
105
|
if (typeof storageInstance.saveRaw !== 'function') {
|
|
@@ -79,7 +116,6 @@ module.exports = function (req, res, next) {
|
|
|
79
116
|
return redirectToOriginal();
|
|
80
117
|
}
|
|
81
118
|
|
|
82
|
-
const imagePath = path.relative(sizeImageDir, req.url);
|
|
83
119
|
const {dir, name, ext} = path.parse(imagePath);
|
|
84
120
|
const [imageNameMatched, imageName, imageNumber] = name.match(/^(.+?)(-\d+)?$/) || [null];
|
|
85
121
|
|
|
@@ -104,12 +140,16 @@ module.exports = function (req, res, next) {
|
|
|
104
140
|
if (originalImageBuffer.length <= 0) {
|
|
105
141
|
throw new NoContentError();
|
|
106
142
|
}
|
|
107
|
-
return imageTransform.resizeFromBuffer(originalImageBuffer, imageDimensionConfig);
|
|
143
|
+
return imageTransform.resizeFromBuffer(originalImageBuffer, {withoutEnlargement: requestUrlFileExtension !== '.svg', ...imageDimensionConfig, format});
|
|
108
144
|
})
|
|
109
145
|
.then((resizedImageBuffer) => {
|
|
110
146
|
return storageInstance.saveRaw(resizedImageBuffer, req.url);
|
|
111
147
|
});
|
|
112
148
|
}).then(() => {
|
|
149
|
+
if (format) {
|
|
150
|
+
// File extension won't match the new format, so we need to update the Content-Type header manually here
|
|
151
|
+
res.type(format);
|
|
152
|
+
}
|
|
113
153
|
next();
|
|
114
154
|
}).catch(function (err) {
|
|
115
155
|
if (err.code === 'SHARP_INSTALLATION' || err.code === 'IMAGE_PROCESSING' || err.errorType === 'NoContentError') {
|
|
@@ -3,7 +3,6 @@ const path = require('path');
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const config = require('../../../shared/config');
|
|
5
5
|
const {blogIcon} = require('../../../server/lib/image');
|
|
6
|
-
const storage = require('../../../server/adapters/storage');
|
|
7
6
|
const urlUtils = require('../../../shared/url-utils');
|
|
8
7
|
const settingsCache = require('../../../shared/settings-cache');
|
|
9
8
|
|
|
@@ -26,11 +25,10 @@ const buildContentResponse = (ext, buf) => {
|
|
|
26
25
|
// ### serveFavicon Middleware
|
|
27
26
|
// Handles requests to favicon.png and favicon.ico
|
|
28
27
|
function serveFavicon() {
|
|
29
|
-
let iconType;
|
|
30
28
|
let filePath;
|
|
31
29
|
|
|
32
30
|
return function serveFaviconMiddleware(req, res, next) {
|
|
33
|
-
if (req.path.match(/^\/favicon\.(ico|png)/i)) {
|
|
31
|
+
if (req.path.match(/^\/favicon\.(ico|png|jpe?g)/i)) {
|
|
34
32
|
// CASE: favicon is default
|
|
35
33
|
// confusing: if you upload an icon, it's same logic as storing images
|
|
36
34
|
// we store as /content/images, because this is the url path images get requested via the browser
|
|
@@ -44,21 +42,8 @@ function serveFavicon() {
|
|
|
44
42
|
|
|
45
43
|
// CASE: custom favicon exists, load it from local file storage
|
|
46
44
|
if (settingsCache.get('icon')) {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
return res.redirect(302, urlUtils.urlFor({relativeUrl: `/favicon${originalExtension}`}));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
storage.getStorage('images')
|
|
53
|
-
.read({path: filePath})
|
|
54
|
-
.then((buf) => {
|
|
55
|
-
iconType = blogIcon.getIconType();
|
|
56
|
-
content = buildContentResponse(iconType, buf);
|
|
57
|
-
|
|
58
|
-
res.writeHead(200, content.headers);
|
|
59
|
-
res.end(content.body);
|
|
60
|
-
})
|
|
61
|
-
.catch(next);
|
|
45
|
+
// Always redirect to the icon path, which is never favicon.xxx
|
|
46
|
+
return res.redirect(302, blogIcon.getIconUrl());
|
|
62
47
|
} else {
|
|
63
48
|
originalExtension = path.extname(filePath).toLowerCase();
|
|
64
49
|
|
|
@@ -13,7 +13,6 @@ const url = require('../utils/url');
|
|
|
13
13
|
const utils = require('../../../index');
|
|
14
14
|
|
|
15
15
|
const postsMetaSchema = require('../../../../../../data/schema').tables.posts_meta;
|
|
16
|
-
const labsService = require('../../../../../../../shared/labs');
|
|
17
16
|
|
|
18
17
|
const getPostServiceInstance = require('../../../../../../services/posts/posts-service');
|
|
19
18
|
const postsService = getPostServiceInstance();
|
|
@@ -35,9 +34,7 @@ module.exports = async (model, frame, options = {}) => {
|
|
|
35
34
|
extraAttrs.forPost(frame, model, jsonModel);
|
|
36
35
|
|
|
37
36
|
// Attach tiers to custom nql visibility filter
|
|
38
|
-
if (
|
|
39
|
-
&& jsonModel.visibility
|
|
40
|
-
) {
|
|
37
|
+
if (jsonModel.visibility) {
|
|
41
38
|
if (['members', 'public'].includes(jsonModel.visibility) && jsonModel.tiers) {
|
|
42
39
|
jsonModel.tiers = tiersData || [];
|
|
43
40
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
//@ts-check
|
|
2
2
|
const debug = require('@tryghost/debug')('api:canary:utils:serializers:output:members');
|
|
3
3
|
const {unparse} = require('@tryghost/members-csv');
|
|
4
|
-
const labsService = require('../../../../../../shared/labs');
|
|
5
4
|
|
|
6
5
|
module.exports = {
|
|
7
6
|
browse: createSerializer('browse', paginatedMembers),
|
|
@@ -142,19 +141,17 @@ function serializeMember(member, options) {
|
|
|
142
141
|
delete subscription.price.product;
|
|
143
142
|
}
|
|
144
143
|
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
serialized.subscribed = true;
|
|
157
|
-
}
|
|
144
|
+
if (json.newsletters) {
|
|
145
|
+
serialized.newsletters = json.newsletters
|
|
146
|
+
.filter(newsletter => newsletter.status === 'active')
|
|
147
|
+
.sort((a, b) => {
|
|
148
|
+
return a.sort_order - b.sort_order;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// override the `subscribed` param to mean "subscribed to any active newsletter"
|
|
152
|
+
serialized.subscribed = false;
|
|
153
|
+
if (Array.isArray(serialized.newsletters) && serialized.newsletters.length > 0) {
|
|
154
|
+
serialized.subscribed = true;
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
return serialized;
|
|
@@ -33,8 +33,21 @@ function serializeSettings(models, apiConfig, frame) {
|
|
|
33
33
|
// If this is public, we already have the right data, we just need to add an Array wrapper
|
|
34
34
|
if (utils.isContentAPI(frame)) {
|
|
35
35
|
filteredSettings = models;
|
|
36
|
+
|
|
37
|
+
// Change the returned icon location to use a resized version, to prevent serving giant icon files
|
|
38
|
+
const icon = filteredSettings.icon;
|
|
39
|
+
if (icon) {
|
|
40
|
+
filteredSettings.icon = filteredSettings.icon.replace(/\/content\/images\//, '/content/images/size/w256h256/');
|
|
41
|
+
}
|
|
36
42
|
} else {
|
|
37
43
|
filteredSettings = _.values(settingsFilter(models, frame.options.group));
|
|
44
|
+
|
|
45
|
+
// Change the returned icon location to use a resized version, to prevent serving giant icon files
|
|
46
|
+
// in admin
|
|
47
|
+
const icon = filteredSettings.find(setting => setting.key === 'icon');
|
|
48
|
+
if (icon && icon.value) {
|
|
49
|
+
icon.value = icon.value.replace(/\/content\/images\//, '/content/images/size/w256h256/');
|
|
50
|
+
}
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
frame.response = {
|
|
@@ -6,7 +6,8 @@ const {imageSize, blogIcon} = require('../../../../../lib/image');
|
|
|
6
6
|
|
|
7
7
|
const messages = {
|
|
8
8
|
isNotSquare: 'Please select a valid image file with square dimensions.',
|
|
9
|
-
|
|
9
|
+
invalidIcoFile: 'Ico icons must be square, at least 60x60px, and under 200kB.',
|
|
10
|
+
invalidFile: 'Icon must be a .jpg, .webp, .svg or .png file, at least 60x60px, under 20MB.'
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
const profileImage = (frame) => {
|
|
@@ -26,14 +27,25 @@ const profileImage = (frame) => {
|
|
|
26
27
|
const icon = (frame) => {
|
|
27
28
|
const iconExtensions = (config.get('uploads').icons && config.get('uploads').icons.extensions) || [];
|
|
28
29
|
|
|
30
|
+
// We don't support resizing .ico files, so we set a lower max upload size
|
|
31
|
+
const isIco = frame.file.ext.toLowerCase() === '.ico';
|
|
32
|
+
const isSVG = frame.file.ext.toLowerCase() === '.svg';
|
|
33
|
+
|
|
29
34
|
const validIconFileSize = (size) => {
|
|
30
|
-
|
|
35
|
+
if (isIco) {
|
|
36
|
+
// Keep using kB instead of KB
|
|
37
|
+
return (size / 1024) <= 200;
|
|
38
|
+
}
|
|
39
|
+
// Use MB representation (not MiB)
|
|
40
|
+
return (size / 1000 / 1000) <= 20;
|
|
31
41
|
};
|
|
32
42
|
|
|
33
|
-
|
|
43
|
+
const message = isIco ? messages.invalidIcoFile : messages.invalidFile;
|
|
44
|
+
|
|
45
|
+
// CASE: file should not be larger than 20MB
|
|
34
46
|
if (!validIconFileSize(frame.file.size)) {
|
|
35
47
|
return Promise.reject(new errors.ValidationError({
|
|
36
|
-
message: tpl(
|
|
48
|
+
message: tpl(message, {extensions: iconExtensions})
|
|
37
49
|
}));
|
|
38
50
|
}
|
|
39
51
|
|
|
@@ -41,25 +53,27 @@ const icon = (frame) => {
|
|
|
41
53
|
// save the image dimensions in new property for file
|
|
42
54
|
frame.file.dimensions = response;
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
if (isIco) {
|
|
57
|
+
// CASE: file needs to be a square
|
|
58
|
+
if (frame.file.dimensions.width !== frame.file.dimensions.height) {
|
|
59
|
+
return Promise.reject(new errors.ValidationError({
|
|
60
|
+
message: tpl(message, {extensions: iconExtensions})
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// CASE: icon needs to be smaller than or equal to 1000px
|
|
65
|
+
if (frame.file.dimensions.width > 1000) {
|
|
66
|
+
return Promise.reject(new errors.ValidationError({
|
|
67
|
+
message: tpl(message, {extensions: iconExtensions})
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
// CASE: icon needs to be bigger than or equal to 60px
|
|
52
73
|
// .ico files can contain multiple sizes, we need at least a minimum of 60px (16px is ok, as long as 60px are present as well)
|
|
53
|
-
if (frame.file.dimensions.width < 60) {
|
|
54
|
-
return Promise.reject(new errors.ValidationError({
|
|
55
|
-
message: tpl(messages.invalidFile, {extensions: iconExtensions})
|
|
56
|
-
}));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// CASE: icon needs to be smaller than or equal to 1000px
|
|
60
|
-
if (frame.file.dimensions.width > 1000) {
|
|
74
|
+
if (!isSVG && frame.file.dimensions.width < 60) {
|
|
61
75
|
return Promise.reject(new errors.ValidationError({
|
|
62
|
-
message: tpl(
|
|
76
|
+
message: tpl(message, {extensions: iconExtensions})
|
|
63
77
|
}));
|
|
64
78
|
}
|
|
65
79
|
});
|
|
@@ -45,6 +45,12 @@ module.exports = {
|
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
// Prevent setting icon to the resized one when sending all settings received from browse again in the edit endpoint
|
|
49
|
+
const icon = frame.data.settings.find(setting => setting.key === 'icon');
|
|
50
|
+
if (icon && icon.value) {
|
|
51
|
+
icon.value = icon.value.replace(/\/content\/images\/size\/([^/]+)\//, '/content/images/');
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
if (errors.length) {
|
|
49
55
|
return Promise.reject(errors[0]);
|
|
50
56
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
1
2
|
const knex = require('knex');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const logging = require('@tryghost/logging');
|
|
2
6
|
const config = require('../../../shared/config');
|
|
3
7
|
let knexInstance;
|
|
4
8
|
|
|
@@ -30,6 +34,15 @@ function configure(dbConfig) {
|
|
|
30
34
|
// Force bthreads to use child_process backend until a worker_thread-compatible version of sqlite3 is published
|
|
31
35
|
// https://github.com/mapbox/node-sqlite3/issues/1386
|
|
32
36
|
process.env.BTHREADS_BACKEND = 'child_process';
|
|
37
|
+
|
|
38
|
+
// In the default SQLite test config we set the path to /tmp/ghost-test.db,
|
|
39
|
+
// but this won't work on Windows, so we need to replace the /tmp bit with
|
|
40
|
+
// the Windows temp folder
|
|
41
|
+
const filename = dbConfig.connection.filename;
|
|
42
|
+
if (process.platform === 'win32' && _.isString(filename) && filename.match(/^\/tmp/)) {
|
|
43
|
+
dbConfig.connection.filename = filename.replace(/^\/tmp/, os.tmpdir());
|
|
44
|
+
logging.info(`Ghost DB path: ${dbConfig.connection.filename}`);
|
|
45
|
+
}
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
if (client === 'mysql2') {
|
|
@@ -56,53 +56,77 @@ class BlogIcon {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* @
|
|
62
|
-
* @returns {boolean} true if submitted path is .ico file
|
|
59
|
+
* Returns the mime type (part after image/) of the favicon that will get served (not the stored one)
|
|
60
|
+
* @param {string} [icon]
|
|
61
|
+
* @returns {'png' | 'x-icon' | 'jpeg'}
|
|
63
62
|
* @description Takes a path and returns boolean value.
|
|
64
63
|
*/
|
|
65
|
-
|
|
66
|
-
const
|
|
64
|
+
getIconType(icon) {
|
|
65
|
+
const ext = this.getIconExt(icon);
|
|
67
66
|
|
|
68
|
-
return
|
|
67
|
+
return ext === 'ico' ? 'x-icon' : ext;
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* @param {string} icon
|
|
75
|
-
* @returns {
|
|
76
|
-
* @description Takes a path and returns boolean value.
|
|
71
|
+
* We support the usage of .svg, .gif, .webp extensions, but (for now, until more browser support them) transform them to
|
|
72
|
+
* a simular extension
|
|
73
|
+
* @param {string} [icon]
|
|
74
|
+
* @returns {'png' | 'ico' | 'jpeg'}
|
|
77
75
|
*/
|
|
78
|
-
|
|
76
|
+
getIconExt(icon) {
|
|
79
77
|
const blogIcon = icon || this.settingsCache.get('icon');
|
|
80
78
|
|
|
81
|
-
|
|
79
|
+
// If the native format is supported, return the native format
|
|
80
|
+
if (blogIcon.match(/.ico$/i)) {
|
|
81
|
+
return 'ico';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (blogIcon.match(/.jpe?g$/i)) {
|
|
85
|
+
return 'jpeg';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (blogIcon.match(/.png$/i)) {
|
|
89
|
+
return 'png';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Default to png for all other types
|
|
93
|
+
return 'png';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getSourceIconExt(icon) {
|
|
97
|
+
const blogIcon = icon || this.settingsCache.get('icon');
|
|
98
|
+
return path.extname(blogIcon).toLowerCase().substring(1);
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
/**
|
|
85
|
-
* Return URL for Blog icon: [subdirectory or not]favicon.[ico or png]
|
|
102
|
+
* Return URL for Blog icon: [subdirectory or not]favicon.[ico, jpeg, or png]
|
|
86
103
|
* Always returns {string} getIconUrl
|
|
87
|
-
* @returns {string} [subdirectory or not]favicon.[ico or png]
|
|
104
|
+
* @returns {string} [subdirectory or not]favicon.[ico, jpeg, or png]
|
|
88
105
|
* @description Checks if we have a custom uploaded icon and the extension of it. If no custom uploaded icon
|
|
89
106
|
* exists, we're returning the default `favicon.ico`
|
|
90
107
|
*/
|
|
91
|
-
getIconUrl(
|
|
108
|
+
getIconUrl(absolute) {
|
|
92
109
|
const blogIcon = this.settingsCache.get('icon');
|
|
93
110
|
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
if (blogIcon) {
|
|
112
|
+
// Resize + format icon to one of the supported file extensions
|
|
113
|
+
const sourceExt = this.getSourceIconExt(blogIcon);
|
|
114
|
+
const destintationExt = this.getIconExt(blogIcon);
|
|
115
|
+
|
|
116
|
+
if (sourceExt === 'ico') {
|
|
117
|
+
// Resize not supported (prevent a redirect)
|
|
118
|
+
return this.urlUtils.urlFor({relativeUrl: blogIcon}, absolute ? true : undefined);
|
|
99
119
|
}
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return this.urlUtils.urlFor({relativeUrl: '/favicon.ico'});
|
|
120
|
+
|
|
121
|
+
if (sourceExt !== destintationExt) {
|
|
122
|
+
const formattedIcon = blogIcon.replace(/\/content\/images\//, `/content/images/size/w256h256/format/${this.getIconExt(blogIcon)}/`);
|
|
123
|
+
return this.urlUtils.urlFor({relativeUrl: formattedIcon}, absolute ? true : undefined);
|
|
105
124
|
}
|
|
125
|
+
|
|
126
|
+
const sizedIcon = blogIcon.replace(/\/content\/images\//, '/content/images/size/w256h256/');
|
|
127
|
+
return this.urlUtils.urlFor({relativeUrl: sizedIcon}, absolute ? true : undefined);
|
|
128
|
+
} else {
|
|
129
|
+
return this.urlUtils.urlFor({relativeUrl: '/favicon.ico'}, absolute ? true : undefined);
|
|
106
130
|
}
|
|
107
131
|
}
|
|
108
132
|
|
|
@@ -38,7 +38,7 @@ module.exports = {
|
|
|
38
38
|
|
|
39
39
|
// NOTE: the "saveRaw" check is smelly
|
|
40
40
|
return imageTransform.canTransformFiles()
|
|
41
|
-
&& imageTransform.
|
|
41
|
+
&& imageTransform.shouldResizeFileExtension(ext)
|
|
42
42
|
&& typeof storage.getStorage('images').saveRaw === 'function';
|
|
43
43
|
}
|
|
44
44
|
});
|
|
@@ -61,6 +61,8 @@ ghostBookshelf.plugin(require('./plugins/data-manipulation'));
|
|
|
61
61
|
|
|
62
62
|
ghostBookshelf.plugin(require('./plugins/overrides'));
|
|
63
63
|
|
|
64
|
+
ghostBookshelf.plugin(require('./plugins/relations'));
|
|
65
|
+
|
|
64
66
|
// Manages nested updates (relationships)
|
|
65
67
|
ghostBookshelf.plugin('bookshelf-relations', {
|
|
66
68
|
allowedOptions: ['context', 'importing', 'migrating'],
|
|
@@ -31,6 +31,7 @@ module.exports = function (Bookshelf) {
|
|
|
31
31
|
|
|
32
32
|
_.each(attrs, function each(value, key) {
|
|
33
33
|
if (value !== null
|
|
34
|
+
&& Object.prototype.hasOwnProperty.call(schema.tables, self.tableName)
|
|
34
35
|
&& Object.prototype.hasOwnProperty.call(schema.tables[self.tableName], key)
|
|
35
36
|
&& schema.tables[self.tableName][key].type === 'dateTime') {
|
|
36
37
|
attrs[key] = moment(value).format('YYYY-MM-DD HH:mm:ss');
|