🎨 Reduced favicon requirements and added image formatting (#14918)

fixes https://github.com/TryGhost/Team/issues/1652
fixes https://github.com/TryGhost/Ghost/issues/13319

**Image formatting**
Added support for changing the format of images via the `handle-image-sizes` middleware (e.g. format SVG to png, jpeg, webp)

This change was required:
- Not all browsers support SVG favicons, so we need to convert them to PNGs
- We can't fit image resizing and formatting in the `serve-favicon` middleware: we need to store the resized image to avoid resizing on every request. This system was already present in the `handle-image-sizes` middleware.

To format an uploaded image:
- Original URL: https://localhost/blog/content/images/2022/05/giphy.gif
- To resize: https://localhost/blog/content/images/size/w256h256/2022/05/giphy.gif (already supported)
- To resize and format to webp: https://localhost/blog/content/images/size/w256h256/format/webp/2022/05/giphy.gif
- Animations are preserved when converting Gifs to Webp and in reverse, and also when only resizing (https://github.com/TryGhost/Ghost/issues/13319)

**Favicons**
- Custom favicons are no longer served via `/favicon.png` or `/favicon.ico` (only for default favicon), but use their full path
- Added support for uploading more image extensions in Ghost as a favicon: .jpg, .jpeg, .gif, .webp and .svg are now supported (already supported .png and .ico).
- File extensions other than jpg/jpeg, png, or ico will always get transformed to the image/png format to guarantee browser support (webp and svg images are not yet supported as favicons by all browsers).

For all image formats, other than .ico files:
- Allowed to upload images larger than 1000px in width and height, they will get cropped to 256x256px.
- Allowed uploading favicons that are not square. They will get cropped automatically.
- Allowed to upload larger files, up to 20MB (will get served at a lower file size after being resized)

For .svg files:
- The minimum size of 60x60px is no longer required.

For .ico files:
- The file size limit is increased to 200kb (coming from 100kb)
This commit is contained in:
Simon Backx 2022-05-27 16:36:53 +02:00 committed by GitHub
parent f805f1637c
commit a051ab3b69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 955 additions and 217 deletions

View File

@ -7,6 +7,8 @@ const activeTheme = require('../../services/theme-engine/active');
const config = require('../../../shared/config');
const SIZE_PATH_REGEX = /^\/size\/([^/]+)\//;
const FORMAT_PATH_REGEX = /^\/format\/([^./]+)\//;
const TRAILING_SLASH_REGEX = /\/+$/;
module.exports = function (req, res, next) {
@ -18,9 +20,29 @@ module.exports = function (req, res, next) {
return next();
}
const [sizeImageDir, requestedDimension] = req.url.match(SIZE_PATH_REGEX);
const requestedDimension = req.url.match(SIZE_PATH_REGEX)[1];
// Note that we don't use sizeImageDir because we need to keep the trailing slash
let imagePath = req.url.replace(`/size/${requestedDimension}`, '');
// Check if we want to format the image
let format = null;
const matchedFormat = imagePath.match(FORMAT_PATH_REGEX);
if (matchedFormat) {
format = matchedFormat[1];
// Note that we don't use matchedFormat[0] because we need to keep the trailing slash
imagePath = imagePath.replace(`/format/${format}`, '');
}
const redirectToOriginal = () => {
const url = req.originalUrl.replace(`/size/${requestedDimension}`, '');
// We need to keep the first slash here
let url = req.originalUrl
.replace(`/size/${requestedDimension}`, '');
if (format) {
url = url.replace(`/format/${format}`, '');
}
return res.redirect(url);
};
@ -31,14 +53,10 @@ module.exports = function (req, res, next) {
return next();
}
// CASE: image transform is not capable of transforming file (e.g. .gif)
if (!imageTransform.canTransformFileExtension(requestUrlFileExtension)) {
return redirectToOriginal();
}
const contentImageSizes = config.get('imageOptimization:contentImageSizes');
const internalImageSizes = config.get('imageOptimization:internalImageSizes');
const themeImageSizes = activeTheme.get().config('image_sizes');
const imageSizes = _.merge({}, themeImageSizes, contentImageSizes);
const imageSizes = _.merge({}, themeImageSizes, internalImageSizes, contentImageSizes);
// CASE: no image_sizes config (NOTE - unlikely to be reachable now we have content sizes)
if (!imageSizes) {
@ -63,6 +81,25 @@ module.exports = function (req, res, next) {
return redirectToOriginal();
}
// CASE: image transform is not capable of transforming some files (e.g. .ico)
if (!imageTransform.canTransformFileExtension(requestUrlFileExtension)) {
return redirectToOriginal();
}
if (format) {
// CASE: When formatting, we need to check if the imageTransform package supports this specific format
if (!imageTransform.canTransformToFormat(format)) {
// transform not supported
return redirectToOriginal();
}
}
// CASE: when transforming is supported, we need to check if it is desired
// (e.g. it is not desired to resize SVGs when not formatting them to a different type)
if (!format && !imageTransform.shouldResizeFileExtension(requestUrlFileExtension)) {
return redirectToOriginal();
}
const storageInstance = storage.getStorage('images');
// CASE: unsupported storage adapter
if (typeof storageInstance.saveRaw !== 'function') {
@ -79,7 +116,6 @@ module.exports = function (req, res, next) {
return redirectToOriginal();
}
const imagePath = path.relative(sizeImageDir, req.url);
const {dir, name, ext} = path.parse(imagePath);
const [imageNameMatched, imageName, imageNumber] = name.match(/^(.+?)(-\d+)?$/) || [null];
@ -104,12 +140,16 @@ module.exports = function (req, res, next) {
if (originalImageBuffer.length <= 0) {
throw new NoContentError();
}
return imageTransform.resizeFromBuffer(originalImageBuffer, imageDimensionConfig);
return imageTransform.resizeFromBuffer(originalImageBuffer, {withoutEnlargement: requestUrlFileExtension !== '.svg', ...imageDimensionConfig, format});
})
.then((resizedImageBuffer) => {
return storageInstance.saveRaw(resizedImageBuffer, req.url);
});
}).then(() => {
if (format) {
// File extension won't match the new format, so we need to update the Content-Type header manually here
res.type(format);
}
next();
}).catch(function (err) {
if (err.code === 'SHARP_INSTALLATION' || err.code === 'IMAGE_PROCESSING' || err.errorType === 'NoContentError') {

View File

@ -3,7 +3,6 @@ const path = require('path');
const crypto = require('crypto');
const config = require('../../../shared/config');
const {blogIcon} = require('../../../server/lib/image');
const storage = require('../../../server/adapters/storage');
const urlUtils = require('../../../shared/url-utils');
const settingsCache = require('../../../shared/settings-cache');
@ -26,11 +25,10 @@ const buildContentResponse = (ext, buf) => {
// ### serveFavicon Middleware
// Handles requests to favicon.png and favicon.ico
function serveFavicon() {
let iconType;
let filePath;
return function serveFaviconMiddleware(req, res, next) {
if (req.path.match(/^\/favicon\.(ico|png)/i)) {
if (req.path.match(/^\/favicon\.(ico|png|jpe?g)/i)) {
// CASE: favicon is default
// confusing: if you upload an icon, it's same logic as storing images
// we store as /content/images, because this is the url path images get requested via the browser
@ -44,21 +42,8 @@ function serveFavicon() {
// CASE: custom favicon exists, load it from local file storage
if (settingsCache.get('icon')) {
// depends on the uploaded icon extension
if (originalExtension !== requestedExtension) {
return res.redirect(302, urlUtils.urlFor({relativeUrl: `/favicon${originalExtension}`}));
}
storage.getStorage('images')
.read({path: filePath})
.then((buf) => {
iconType = blogIcon.getIconType();
content = buildContentResponse(iconType, buf);
res.writeHead(200, content.headers);
res.end(content.body);
})
.catch(next);
// Always redirect to the icon path, which is never favicon.xxx
return res.redirect(302, blogIcon.getIconUrl());
} else {
originalExtension = path.extname(filePath).toLowerCase();

View File

@ -33,8 +33,21 @@ function serializeSettings(models, apiConfig, frame) {
// If this is public, we already have the right data, we just need to add an Array wrapper
if (utils.isContentAPI(frame)) {
filteredSettings = models;
// Change the returned icon location to use a resized version, to prevent serving giant icon files
const icon = filteredSettings.icon;
if (icon) {
filteredSettings.icon = filteredSettings.icon.replace(/\/content\/images\//, '/content/images/size/w256h256/');
}
} else {
filteredSettings = _.values(settingsFilter(models, frame.options.group));
// Change the returned icon location to use a resized version, to prevent serving giant icon files
// in admin
const icon = filteredSettings.find(setting => setting.key === 'icon');
if (icon && icon.value) {
icon.value = icon.value.replace(/\/content\/images\//, '/content/images/size/w256h256/');
}
}
frame.response = {

View File

@ -6,7 +6,8 @@ const {imageSize, blogIcon} = require('../../../../../lib/image');
const messages = {
isNotSquare: 'Please select a valid image file with square dimensions.',
invalidFile: 'Icon must be a square .ico or .png file between 60px 1,000px, under 100kb.'
invalidIcoFile: 'Ico icons must be square, at least 60x60px, and under 200kB.',
invalidFile: 'Icon must be a .jpg, .webp, .svg or .png file, at least 60x60px, under 20MB.'
};
const profileImage = (frame) => {
@ -26,14 +27,25 @@ const profileImage = (frame) => {
const icon = (frame) => {
const iconExtensions = (config.get('uploads').icons && config.get('uploads').icons.extensions) || [];
// We don't support resizing .ico files, so we set a lower max upload size
const isIco = frame.file.ext.toLowerCase() === '.ico';
const isSVG = frame.file.ext.toLowerCase() === '.svg';
const validIconFileSize = (size) => {
return (size / 1024) <= 100;
if (isIco) {
// Keep using kB instead of KB
return (size / 1024) <= 200;
}
// Use MB representation (not MiB)
return (size / 1000 / 1000) <= 20;
};
// CASE: file should not be larger than 100kb
const message = isIco ? messages.invalidIcoFile : messages.invalidFile;
// CASE: file should not be larger than 20MB
if (!validIconFileSize(frame.file.size)) {
return Promise.reject(new errors.ValidationError({
message: tpl(messages.invalidFile, {extensions: iconExtensions})
message: tpl(message, {extensions: iconExtensions})
}));
}
@ -41,25 +53,27 @@ const icon = (frame) => {
// save the image dimensions in new property for file
frame.file.dimensions = response;
// CASE: file needs to be a square
if (frame.file.dimensions.width !== frame.file.dimensions.height) {
return Promise.reject(new errors.ValidationError({
message: tpl(messages.invalidFile, {extensions: iconExtensions})
}));
if (isIco) {
// CASE: file needs to be a square
if (frame.file.dimensions.width !== frame.file.dimensions.height) {
return Promise.reject(new errors.ValidationError({
message: tpl(message, {extensions: iconExtensions})
}));
}
// CASE: icon needs to be smaller than or equal to 1000px
if (frame.file.dimensions.width > 1000) {
return Promise.reject(new errors.ValidationError({
message: tpl(message, {extensions: iconExtensions})
}));
}
}
// CASE: icon needs to be bigger than or equal to 60px
// .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)
if (frame.file.dimensions.width < 60) {
if (!isSVG && frame.file.dimensions.width < 60) {
return Promise.reject(new errors.ValidationError({
message: tpl(messages.invalidFile, {extensions: iconExtensions})
}));
}
// CASE: icon needs to be smaller than or equal to 1000px
if (frame.file.dimensions.width > 1000) {
return Promise.reject(new errors.ValidationError({
message: tpl(messages.invalidFile, {extensions: iconExtensions})
message: tpl(message, {extensions: iconExtensions})
}));
}
});

View File

@ -45,6 +45,12 @@ module.exports = {
}
});
// Prevent setting icon to the resized one when sending all settings received from browse again in the edit endpoint
const icon = frame.data.settings.find(setting => setting.key === 'icon');
if (icon && icon.value) {
icon.value = icon.value.replace(/\/content\/images\/size\/([^/]+)\//, '/content/images/');
}
if (errors.length) {
return Promise.reject(errors[0]);
}

View File

@ -56,53 +56,77 @@ class BlogIcon {
}
/**
* Check if file is `.ico` extension
* Always returns {object} isIcoImageType
* @param {string} icon
* @returns {boolean} true if submitted path is .ico file
* @description Takes a path and returns boolean value.
*/
isIcoImageType(icon) {
const blogIcon = icon || this.settingsCache.get('icon');
return blogIcon.match(/.ico$/i) ? true : false;
}
/**
* Check if file is `.ico` extension
* Always returns {object} isIcoImageType
* @param {string} icon
* @returns {boolean} true if submitted path is .ico file
* Returns the mime type (part after image/) of the favicon that will get served (not the stored one)
* @param {string} [icon]
* @returns {'png' | 'x-icon' | 'jpeg'}
* @description Takes a path and returns boolean value.
*/
getIconType(icon) {
const blogIcon = icon || this.settingsCache.get('icon');
const ext = this.getIconExt(icon);
return this.isIcoImageType(blogIcon) ? 'x-icon' : 'png';
return ext === 'ico' ? 'x-icon' : ext;
}
/**
* Return URL for Blog icon: [subdirectory or not]favicon.[ico or png]
* We support the usage of .svg, .gif, .webp extensions, but (for now, until more browser support them) transform them to
* a simular extension
* @param {string} [icon]
* @returns {'png' | 'ico' | 'jpeg'}
*/
getIconExt(icon) {
const blogIcon = icon || this.settingsCache.get('icon');
// If the native format is supported, return the native format
if (blogIcon.match(/.ico$/i)) {
return 'ico';
}
if (blogIcon.match(/.jpe?g$/i)) {
return 'jpeg';
}
if (blogIcon.match(/.png$/i)) {
return 'png';
}
// Default to png for all other types
return 'png';
}
getSourceIconExt(icon) {
const blogIcon = icon || this.settingsCache.get('icon');
return path.extname(blogIcon).toLowerCase().substring(1);
}
/**
* Return URL for Blog icon: [subdirectory or not]favicon.[ico, jpeg, or png]
* Always returns {string} getIconUrl
* @returns {string} [subdirectory or not]favicon.[ico or png]
* @returns {string} [subdirectory or not]favicon.[ico, jpeg, or png]
* @description Checks if we have a custom uploaded icon and the extension of it. If no custom uploaded icon
* exists, we're returning the default `favicon.ico`
*/
getIconUrl(absolut) {
getIconUrl(absolute) {
const blogIcon = this.settingsCache.get('icon');
if (absolut) {
if (blogIcon) {
return this.isIcoImageType(blogIcon) ? this.urlUtils.urlFor({relativeUrl: '/favicon.ico'}, true) : this.urlUtils.urlFor({relativeUrl: '/favicon.png'}, true);
} else {
return this.urlUtils.urlFor({relativeUrl: '/favicon.ico'}, true);
if (blogIcon) {
// Resize + format icon to one of the supported file extensions
const sourceExt = this.getSourceIconExt(blogIcon);
const destintationExt = this.getIconExt(blogIcon);
if (sourceExt === 'ico') {
// Resize not supported (prevent a redirect)
return this.urlUtils.urlFor({relativeUrl: blogIcon}, absolute ? true : undefined);
}
if (sourceExt !== destintationExt) {
const formattedIcon = blogIcon.replace(/\/content\/images\//, `/content/images/size/w256h256/format/${this.getIconExt(blogIcon)}/`);
return this.urlUtils.urlFor({relativeUrl: formattedIcon}, absolute ? true : undefined);
}
const sizedIcon = blogIcon.replace(/\/content\/images\//, '/content/images/size/w256h256/');
return this.urlUtils.urlFor({relativeUrl: sizedIcon}, absolute ? true : undefined);
} else {
if (blogIcon) {
return this.isIcoImageType(blogIcon) ? this.urlUtils.urlFor({relativeUrl: '/favicon.ico'}) : this.urlUtils.urlFor({relativeUrl: '/favicon.png'});
} else {
return this.urlUtils.urlFor({relativeUrl: '/favicon.ico'});
}
return this.urlUtils.urlFor({relativeUrl: '/favicon.ico'}, absolute ? true : undefined);
}
}

View File

@ -38,7 +38,7 @@ module.exports = {
// NOTE: the "saveRaw" check is smelly
return imageTransform.canTransformFiles()
&& imageTransform.canTransformFileExtension(ext)
&& imageTransform.shouldResizeFileExtension(ext)
&& typeof storage.getStorage('images').saveRaw === 'function';
}
});

View File

@ -7,7 +7,7 @@ module.exports = function normalize(req, res, next) {
const imageOptimizationOptions = config.get('imageOptimization');
// CASE: image transform is not capable of transforming file (e.g. .gif)
if (!imageTransform.canTransformFileExtension(req.file.ext) || !imageOptimizationOptions.resize) {
if (!imageTransform.shouldResizeFileExtension(req.file.ext) || !imageOptimizationOptions.resize) {
return next();
}

View File

@ -49,8 +49,8 @@
"contentTypes": ["image/jpeg", "image/png", "image/gif", "image/svg+xml", "image/x-icon", "image/vnd.microsoft.icon", "image/webp"]
},
"icons": {
"extensions": [".png", ".ico"],
"contentTypes": ["image/png", "image/x-icon", "image/vnd.microsoft.icon"]
"extensions": [".jpg", ".jpeg", ".gif", ".png", ".svg", ".svgz", ".ico", ".webp"],
"contentTypes": ["image/jpeg", "image/png", "image/gif", "image/svg+xml", "image/x-icon", "image/vnd.microsoft.icon", "image/webp"]
},
"db": {
"extensions": [".json", ".zip"],
@ -84,6 +84,9 @@
"w1000": {"width": 1000},
"w1600": {"width": 1600},
"w2400": {"width": 2400}
},
"internalImageSizes": {
"icon": {"width": 256, "height": 256}
}
}
}

View File

@ -74,7 +74,7 @@
"@tryghost/errors": "1.2.12",
"@tryghost/express-dynamic-redirects": "0.2.13",
"@tryghost/helpers": "1.1.70",
"@tryghost/image-transform": "1.0.32",
"@tryghost/image-transform": "1.1.0",
"@tryghost/job-manager": "0.8.24",
"@tryghost/kg-card-factory": "3.1.3",
"@tryghost/kg-default-atoms": "3.1.2",

View File

@ -650,7 +650,7 @@ Object {
},
Object {
"key": "icon",
"value": "",
"value": "http://127.0.0.1:2369/content/images/size/w256h256/2019/07/icon.png",
},
Object {
"key": "accent_color",
@ -908,7 +908,7 @@ exports[`Settings API Edit cannot edit uneditable settings 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "3297",
"content-length": "3364",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
@ -916,6 +916,295 @@ Object {
}
`;
exports[`Settings API Edit removes image size prefixes when setting the icon 1: [body] 1`] = `
Object {
"meta": Object {},
"settings": Array [
Object {
"key": "title",
"value": "[]",
},
Object {
"key": "description",
"value": "Thoughts, stories and ideas",
},
Object {
"key": "logo",
"value": "",
},
Object {
"key": "cover_image",
"value": "https://static.ghost.org/v4.0.0/images/publication-cover.jpg",
},
Object {
"key": "icon",
"value": "http://127.0.0.1:2369/content/images/size/w256h256/2019/07/icon.png",
},
Object {
"key": "accent_color",
"value": "#FF1A75",
},
Object {
"key": "locale",
"value": "ua",
},
Object {
"key": "timezone",
"value": "Pacific/Auckland",
},
Object {
"key": "codeinjection_head",
"value": null,
},
Object {
"key": "codeinjection_foot",
"value": "",
},
Object {
"key": "facebook",
"value": "ghost",
},
Object {
"key": "twitter",
"value": "@ghost",
},
Object {
"key": "navigation",
"value": "[{\\"label\\":\\"label1\\"}]",
},
Object {
"key": "secondary_navigation",
"value": "[{\\"label\\":\\"Data & privacy\\",\\"url\\":\\"/privacy/\\"},{\\"label\\":\\"Contact\\",\\"url\\":\\"/contact/\\"},{\\"label\\":\\"Contribute →\\",\\"url\\":\\"/contribute/\\"}]",
},
Object {
"key": "meta_title",
"value": "SEO title",
},
Object {
"key": "meta_description",
"value": "SEO description",
},
Object {
"key": "og_image",
"value": "http://127.0.0.1:2369/content/images/2019/07/facebook.png",
},
Object {
"key": "og_title",
"value": "facebook title",
},
Object {
"key": "og_description",
"value": "facebook description",
},
Object {
"key": "twitter_image",
"value": "http://127.0.0.1:2369/content/images/2019/07/twitter.png",
},
Object {
"key": "twitter_title",
"value": "twitter title",
},
Object {
"key": "twitter_description",
"value": "twitter description",
},
Object {
"key": "active_theme",
"value": "casper",
},
Object {
"key": "is_private",
"value": false,
},
Object {
"key": "password",
"value": "",
},
Object {
"key": "public_hash",
"value": StringMatching /\\[a-z0-9\\]\\{30\\}/,
},
Object {
"key": "default_content_visibility",
"value": "public",
},
Object {
"key": "default_content_visibility_tiers",
"value": "[]",
},
Object {
"key": "members_signup_access",
"value": "all",
},
Object {
"key": "members_support_address",
"value": "noreply",
},
Object {
"key": "stripe_secret_key",
"value": null,
},
Object {
"key": "stripe_publishable_key",
"value": null,
},
Object {
"key": "stripe_plans",
"value": "[]",
},
Object {
"key": "stripe_connect_publishable_key",
"value": "pk_test_for_stripe",
},
Object {
"key": "stripe_connect_secret_key",
"value": "••••••••",
},
Object {
"key": "stripe_connect_livemode",
"value": null,
},
Object {
"key": "stripe_connect_display_name",
"value": null,
},
Object {
"key": "stripe_connect_account_id",
"value": null,
},
Object {
"key": "members_monthly_price_id",
"value": null,
},
Object {
"key": "members_yearly_price_id",
"value": null,
},
Object {
"key": "portal_name",
"value": true,
},
Object {
"key": "portal_button",
"value": true,
},
Object {
"key": "portal_plans",
"value": "[\\"free\\"]",
},
Object {
"key": "portal_products",
"value": "[]",
},
Object {
"key": "portal_button_style",
"value": "icon-and-text",
},
Object {
"key": "portal_button_icon",
"value": null,
},
Object {
"key": "portal_button_signup_text",
"value": "Subscribe",
},
Object {
"key": "mailgun_domain",
"value": null,
},
Object {
"key": "mailgun_api_key",
"value": null,
},
Object {
"key": "mailgun_base_url",
"value": null,
},
Object {
"key": "email_track_opens",
"value": true,
},
Object {
"key": "email_verification_required",
"value": false,
},
Object {
"key": "amp",
"value": false,
},
Object {
"key": "amp_gtag_id",
"value": null,
},
Object {
"key": "firstpromoter",
"value": false,
},
Object {
"key": "firstpromoter_id",
"value": null,
},
Object {
"key": "labs",
"value": "{\\"members\\":true}",
},
Object {
"key": "slack_url",
"value": "",
},
Object {
"key": "slack_username",
"value": "New Slack Username",
},
Object {
"key": "unsplash",
"value": false,
},
Object {
"key": "shared_views",
"value": "[]",
},
Object {
"key": "editor_default_email_recipients",
"value": "visibility",
},
Object {
"key": "editor_default_email_recipients_filter",
"value": "all",
},
Object {
"key": "members_enabled",
"value": true,
},
Object {
"key": "members_invite_only",
"value": false,
},
Object {
"key": "paid_members_enabled",
"value": true,
},
Object {
"key": "firstpromoter_account",
"value": null,
},
],
}
`;
exports[`Settings API Edit removes image size prefixes when setting the icon 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "3364",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Origin, Accept-Encoding",
"x-cache-invalidate": "/*",
"x-powered-by": "Express",
}
`;
exports[`Settings API deprecated can do updateMembersEmail 1: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",

View File

@ -1,4 +1,5 @@
const assert = require('assert');
const settingsCache = require('../../../core/shared/settings-cache');
const {agentProvider, fixtureManager, mockManager, matchers} = require('../../utils/e2e-framework');
const {stringMatching, anyEtag, anyUuid} = matchers;
@ -164,6 +165,36 @@ describe('Settings API', function () {
});
});
it('removes image size prefixes when setting the icon', async function () {
const settingsToChange = [
{
key: 'icon',
value: '/content/images/size/w256h256/2019/07/icon.png'
}
];
const {body} = await agent.put('settings/')
.body({
settings: settingsToChange
})
.expectStatus(200)
.matchBodySnapshot({
settings: matchSettingsArray(CURRENT_SETTINGS_COUNT)
})
.matchHeaderSnapshot({
etag: anyEtag
});
// Check returned WITH prefix
const val = body.settings.find(setting => setting.key === 'icon');
assert.ok(val);
assert.equal(val.value, 'http://127.0.0.1:2369/content/images/size/w256h256/2019/07/icon.png');
// Check if not changed (also check internal ones)
const afterValue = settingsCache.get('icon');
assert.equal(afterValue, 'http://127.0.0.1:2369/content/images/2019/07/icon.png');
});
it('cannot edit uneditable settings', async function () {
await agent.put('settings/')
.body({

View File

@ -188,7 +188,7 @@ describe('Settings API (canary)', function () {
responseSettings.should.have.property('cover_image', `${config.get('url')}/content/images/cover_image.png`);
responseSettings.should.have.property('logo', `${config.get('url')}/content/images/logo.png`);
responseSettings.should.have.property('icon', `${config.get('url')}/content/images/icon.png`);
responseSettings.should.have.property('icon', `${config.get('url')}/content/images/size/w256h256/icon.png`);
responseSettings.should.have.property('portal_button_icon', `${config.get('url')}/content/images/portal_button_icon.png`);
responseSettings.should.have.property('og_image', `${config.get('url')}/content/images/og_image.png`);
responseSettings.should.have.property('twitter_image', `${config.get('url')}/content/images/twitter_image.png`);

View File

@ -40,19 +40,29 @@ describe('{{asset}} helper', function () {
});
it('handles custom favicon correctly', function () {
localSettingsCache.icon = '/content/images/favicon.png';
// with png
localSettingsCache.icon = '/content/images/favicon.png';
rendered = asset('favicon.png');
should.exist(rendered);
String(rendered).should.equal('/favicon.png');
localSettingsCache.icon = '/content/images/favicon.ico';
String(rendered).should.equal('/content/images/size/w256h256/favicon.png');
// with ico
localSettingsCache.icon = '/content/images/favicon.ico';
rendered = asset('favicon.ico');
should.exist(rendered);
String(rendered).should.equal('/favicon.ico');
String(rendered).should.equal('/content/images/favicon.ico');
// with webp
localSettingsCache.icon = '/content/images/favicon.webp';
rendered = asset('favicon.png');
should.exist(rendered);
String(rendered).should.equal('/content/images/size/w256h256/format/png/favicon.webp');
// with svg
localSettingsCache.icon = '/content/images/favicon.svg';
rendered = asset('favicon.png');
should.exist(rendered);
String(rendered).should.equal('/content/images/size/w256h256/format/png/favicon.svg');
});
it('handles public assets correctly', function () {

View File

@ -1304,7 +1304,7 @@ describe('{{ghost_head}} helper', function () {
}
})).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="icon" href="\/site\/favicon.png" type="image\/png" \/>/);
rendered.string.should.match(/<link rel="icon" href="\/site\/content\/images\/size\/w256h256\/favicon.png" type="image\/png" \/>/);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/localhost:65530\/site\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/localhost:65530\/site\/rss\/" \/>/);
@ -1333,7 +1333,7 @@ describe('{{ghost_head}} helper', function () {
}
})).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="icon" href="\/site\/favicon.png" type="image\/png" \/>/);
rendered.string.should.match(/<link rel="icon" href="\/site\/content\/images\/size\/w256h256\/favicon.png" type="image\/png" \/>/);
rendered.string.should.match(/<meta name="referrer" content="origin" \/>/);
rendered.string.should.not.match(/<meta name="description" /);
@ -1368,7 +1368,7 @@ describe('{{ghost_head}} helper', function () {
}
})).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="icon" href="\/favicon.png" type="image\/png" \/>/);
rendered.string.should.match(/<link rel="icon" href="\/content\/images\/size\/w256h256\/favicon.png" type="image\/png" \/>/);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/localhost:65530\/post\/" \/>/);
rendered.string.should.match(/<link rel="amphtml" href="http:\/\/localhost:65530\/post\/amp\/" \/>/);
rendered.string.should.match(/<meta name="description" content="site description" \/>/);
@ -1401,7 +1401,7 @@ describe('{{ghost_head}} helper', function () {
}
})).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="icon" href="\/favicon.png" type="image\/png" \/>/);
rendered.string.should.match(/<link rel="icon" href="\/content\/images\/size\/w256h256\/favicon.png" type="image\/png" \/>/);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/localhost:65530\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/localhost:65530\/rss\/" \/>/);
@ -1487,7 +1487,7 @@ describe('{{ghost_head}} helper', function () {
}
})).then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<link rel="icon" href="\/favicon.png" type="image\/png" \/>/);
rendered.string.should.match(/<link rel="icon" href="\/content\/images\/size\/w256h256\/favicon.png" type="image\/png" \/>/);
rendered.string.should.match(/<link rel="canonical" href="http:\/\/localhost:65530\/" \/>/);
rendered.string.should.match(/<meta name="generator" content="Ghost 0.3" \/>/);
rendered.string.should.match(/<link rel="alternate" type="application\/rss\+xml" title="Ghost" href="http:\/\/localhost:65530\/rss\/" \/>/);

View File

@ -52,7 +52,13 @@ describe('getAssetUrl', function () {
it('should correct favicon path for custom png', function () {
sinon.stub(settingsCache, 'get').withArgs('icon').returns('/content/images/2017/04/my-icon.png');
const testUrl = getAssetUrl('favicon.ico');
testUrl.should.equal('/favicon.png');
testUrl.should.equal('/content/images/size/w256h256/2017/04/my-icon.png');
});
it('should correct favicon path for custom svg', function () {
sinon.stub(settingsCache, 'get').withArgs('icon').returns('/content/images/2017/04/my-icon.svg');
const testUrl = getAssetUrl('favicon.ico');
testUrl.should.equal('/content/images/size/w256h256/format/png/2017/04/my-icon.svg');
});
});

View File

@ -35,6 +35,6 @@ describe('getBlogLogo', function () {
blogLogo = getBlogLogo();
should.exist(blogLogo);
blogLogo.should.have.property('url', 'http://127.0.0.1:2369/favicon.png');
blogLogo.should.have.property('url', 'http://127.0.0.1:2369/content/images/size/w256h256/favicon.png');
});
});

View File

@ -3,6 +3,7 @@ const sinon = require('sinon');
const storage = require('../../../../../core/server/adapters/storage');
const activeTheme = require('../../../../../core/frontend/services/theme-engine/active');
const handleImageSizes = require('../../../../../core/frontend/web/middleware/handle-image-sizes.js');
const imageTransform = require('@tryghost/image-transform');
// @TODO make these tests lovely and non specific to implementation
describe('handleImageSizes middleware', function () {
@ -48,15 +49,18 @@ describe('handleImageSizes middleware', function () {
describe('file handling', function () {
let dummyStorage;
let dummyTheme;
let resizeFromBufferStub;
let buffer;
this.beforeEach(function () {
buffer = Buffer.from([0]);
dummyStorage = {
async exists() {
return true;
},
read() {
return Buffer.from([]);
return buffer;
},
async saveRaw(buf, url) {
@ -78,12 +82,36 @@ describe('handleImageSizes middleware', function () {
sinon.stub(storage, 'getStorage').returns(dummyStorage);
sinon.stub(activeTheme, 'get').returns(dummyTheme);
resizeFromBufferStub = sinon.stub(imageTransform, 'resizeFromBuffer').resolves(Buffer.from([]));
});
this.afterEach(function () {
sinon.restore();
});
it('redirects for invalid format extension', function (done) {
const fakeReq = {
url: '/size/w1000/format/test/image.jpg',
originalUrl: '/blog/content/images/size/w1000/format/test/image.jpg'
};
const fakeRes = {
redirect(url) {
try {
url.should.equal('/blog/content/images/image.jpg');
} catch (e) {
return done(e);
}
done();
}
};
handleImageSizes(fakeReq, fakeRes, function next(err) {
if (err) {
return done(err);
}
done(new Error('Should not have called next'));
});
});
it('returns original URL if file is empty', function (done) {
dummyStorage.exists = async function (path) {
if (path === '/blank_o.png') {
@ -93,14 +121,21 @@ describe('handleImageSizes middleware', function () {
return false;
}
};
dummyStorage.read = async function (path) {
return Buffer.from([]);
};
const fakeReq = {
url: '/size/w1000/blank.png',
originalUrl: '/size/w1000/blank.png'
originalUrl: '/blog/content/images/size/w1000/blank.png'
};
const fakeRes = {
redirect(url) {
url.should.equal('/blank.png');
try {
url.should.equal('/blog/content/images/blank.png');
} catch (e) {
return done(e);
}
done();
}
};
@ -112,5 +147,340 @@ describe('handleImageSizes middleware', function () {
done(new Error('Should not have called next'));
});
});
it('continues if file exists', function (done) {
dummyStorage.exists = async function (path) {
if (path === '/size/w1000/blank.png') {
return true;
}
};
const fakeReq = {
url: '/size/w1000/blank.png',
originalUrl: '/size/w1000/blank.png'
};
const fakeRes = {
redirect(url) {
done(new Error('Should not have called redirect'));
}
};
handleImageSizes(fakeReq, fakeRes, function next(err) {
if (err) {
return done(err);
}
done();
});
});
it('uses unoptimizedImageExists if it exists', function (done) {
dummyStorage.exists = async function (path) {
if (path === '/blank_o.png') {
return true;
}
};
const spy = sinon.spy(dummyStorage, 'read');
const fakeReq = {
url: '/size/w1000/blank.png',
originalUrl: '/size/w1000/blank.png'
};
const fakeRes = {
redirect(url) {
done(new Error('Should not have called redirect'));
}
};
handleImageSizes(fakeReq, fakeRes, function next(err) {
if (err) {
return done(err);
}
try {
spy.calledOnceWithExactly({path: '/blank_o.png'}).should.be.true();
} catch (e) {
return done(e);
}
done();
});
});
it('uses unoptimizedImageExists if it exists with formatting', function (done) {
dummyStorage.exists = async function (path) {
if (path === '/blank_o.png') {
return true;
}
};
const spy = sinon.spy(dummyStorage, 'read');
const fakeReq = {
url: '/size/w1000/format/webp/blank.png',
originalUrl: '/size/w1000/format/webp/blank.png'
};
const fakeRes = {
redirect(url) {
done(new Error('Should not have called redirect'));
},
type: function () {}
};
const typeStub = sinon.spy(fakeRes, 'type');
handleImageSizes(fakeReq, fakeRes, function next(err) {
if (err) {
return done(err);
}
try {
spy.calledOnceWithExactly({path: '/blank_o.png'}).should.be.true();
typeStub.calledOnceWithExactly('webp').should.be.true();
} catch (e) {
return done(e);
}
done();
});
});
it('skips SVG if not formatted', function (done) {
dummyStorage.exists = async function (path) {
return false;
};
const fakeReq = {
url: '/size/w1000/blank.svg',
originalUrl: '/blog/content/images/size/w1000/blank.svg'
};
const fakeRes = {
redirect(url) {
try {
url.should.equal('/blog/content/images/blank.svg');
} catch (e) {
return done(e);
}
done();
}
};
handleImageSizes(fakeReq, fakeRes, function next(err) {
if (err) {
return done(err);
}
done(new Error('Should not have called next'));
});
});
it('skips formatting to ico', function (done) {
dummyStorage.exists = async function (path) {
return false;
};
const fakeReq = {
url: '/size/w1000/format/ico/blank.png',
originalUrl: '/blog/content/images/size/w1000/format/ico/blank.png'
};
const fakeRes = {
redirect(url) {
try {
url.should.equal('/blog/content/images/blank.png');
} catch (e) {
return done(e);
}
done();
}
};
handleImageSizes(fakeReq, fakeRes, function next(err) {
if (err) {
return done(err);
}
done(new Error('Should not have called next'));
});
});
it('skips formatting from ico', function (done) {
dummyStorage.exists = async function (path) {
return false;
};
const fakeReq = {
url: '/size/w1000/format/png/blank.ico',
originalUrl: '/blog/content/images/size/w1000/format/png/blank.ico'
};
const fakeRes = {
redirect(url) {
try {
url.should.equal('/blog/content/images/blank.ico');
} catch (e) {
return done(e);
}
done();
}
};
handleImageSizes(fakeReq, fakeRes, function next(err) {
if (err) {
return done(err);
}
done(new Error('Should not have called next'));
});
});
it('skips formatting to svg', function (done) {
dummyStorage.exists = async function (path) {
return false;
};
const fakeReq = {
url: '/size/w1000/format/svg/blank.png',
originalUrl: '/blog/content/images/size/w1000/format/svg/blank.png'
};
const fakeRes = {
redirect(url) {
try {
url.should.equal('/blog/content/images/blank.png');
} catch (e) {
return done(e);
}
done();
}
};
handleImageSizes(fakeReq, fakeRes, function next(err) {
if (err) {
return done(err);
}
done(new Error('Should not have called next'));
});
});
it('doesn\'t skip SVGs if formatted to PNG', function (done) {
dummyStorage.exists = async function (path) {
return false;
};
const fakeReq = {
url: '/size/w1000/format/png/blank.svg',
originalUrl: '/size/w1000/format/png/blank.svg'
};
const fakeRes = {
redirect(url) {
done(new Error('Should not have called redirect'));
},
type: function () {}
};
const typeStub = sinon.spy(fakeRes, 'type');
handleImageSizes(fakeReq, fakeRes, function next(err) {
if (err) {
return done(err);
}
try {
resizeFromBufferStub.calledOnceWithExactly(buffer, {withoutEnlargement: false, width: 1000, format: 'png'}).should.be.true();
typeStub.calledOnceWithExactly('png').should.be.true();
} catch (e) {
return done(e);
}
done();
});
});
it('can format PNG to WEBP', function (done) {
dummyStorage.exists = async function (path) {
return false;
};
dummyStorage.read = async function (path) {
return buffer;
};
const fakeReq = {
url: '/size/w1000/format/webp/blank.png',
originalUrl: '/size/w1000/format/webp/blank.png'
};
const fakeRes = {
redirect(url) {
done(new Error('Should not have called redirect'));
},
type: function () {}
};
const typeStub = sinon.spy(fakeRes, 'type');
handleImageSizes(fakeReq, fakeRes, function next(err) {
if (err) {
return done(err);
}
try {
resizeFromBufferStub.calledOnceWithExactly(buffer, {withoutEnlargement: true, width: 1000, format: 'webp'}).should.be.true();
typeStub.calledOnceWithExactly('webp').should.be.true();
} catch (e) {
return done(e);
}
done();
});
});
it('can format GIF to WEBP', function (done) {
dummyStorage.exists = async function (path) {
return false;
};
dummyStorage.read = async function (path) {
return buffer;
};
const fakeReq = {
url: '/size/w1000/format/webp/blank.gif',
originalUrl: '/size/w1000/format/webp/blank.gif'
};
const fakeRes = {
redirect(url) {
done(new Error('Should not have called redirect'));
},
type: function () {}
};
const typeStub = sinon.spy(fakeRes, 'type');
handleImageSizes(fakeReq, fakeRes, function next(err) {
if (err) {
return done(err);
}
try {
resizeFromBufferStub.calledOnceWithExactly(buffer, {withoutEnlargement: true, width: 1000, format: 'webp'}).should.be.true();
typeStub.calledOnceWithExactly('webp').should.be.true();
} catch (e) {
return done(e);
}
done();
});
});
it('can format WEBP to GIF', function (done) {
dummyStorage.exists = async function (path) {
return false;
};
dummyStorage.read = async function (path) {
return buffer;
};
const fakeReq = {
url: '/size/w1000/format/gif/blank.webp',
originalUrl: '/size/w1000/format/gif/blank.webp'
};
const fakeRes = {
redirect(url) {
done(new Error('Should not have called redirect'));
},
type: function () {}
};
const typeStub = sinon.spy(fakeRes, 'type');
handleImageSizes(fakeReq, fakeRes, function next(err) {
if (err) {
return done(err);
}
try {
resizeFromBufferStub.calledOnceWithExactly(buffer, {withoutEnlargement: true, width: 1000, format: 'gif'}).should.be.true();
typeStub.calledOnceWithExactly('gif').should.be.true();
} catch (e) {
return done(e);
}
done();
});
});
});
});

View File

@ -51,66 +51,6 @@ describe('Serve Favicon', function () {
});
describe('serves', function () {
it('custom uploaded favicon.png', function (done) {
const middleware = serveFavicon();
req.path = '/favicon.png';
storage.getStorage().storagePath = path.join(__dirname, '../../../../utils/fixtures/images/');
localSettingsCache.icon = 'favicon.png';
res = {
writeHead: function (statusCode) {
statusCode.should.eql(200);
},
end: function (body) {
body.length.should.eql(6792);
done();
}
};
middleware(req, res, next);
});
it('custom uploaded favicon.ico', function (done) {
const middleware = serveFavicon();
req.path = '/favicon.ico';
storage.getStorage().storagePath = path.join(__dirname, '../../../../utils/fixtures/images/');
localSettingsCache.icon = 'favicon.ico';
res = {
writeHead: function (statusCode) {
statusCode.should.eql(200);
},
end: function (body) {
body.length.should.eql(15406);
done();
}
};
middleware(req, res, next);
});
it('custom uploaded myicon.ico', function (done) {
const middleware = serveFavicon();
req.path = '/favicon.ico';
storage.getStorage().storagePath = path.join(__dirname, '../../../../utils/fixtures/images/');
localSettingsCache.icon = 'myicon.ico';
res = {
writeHead: function (statusCode) {
statusCode.should.eql(200);
},
end: function (body) {
body.length.should.eql(15086);
done();
}
};
middleware(req, res, next);
});
it('default favicon.ico', function (done) {
const middleware = serveFavicon();
req.path = '/favicon.ico';
@ -131,16 +71,17 @@ describe('Serve Favicon', function () {
});
describe('redirects', function () {
it('to custom favicon.ico when favicon.png is requested', function (done) {
it('custom uploaded favicon.png', function (done) {
const middleware = serveFavicon();
req.path = '/favicon.png';
configUtils.set('paths:contentPath', path.join(__dirname, '../../../../test/utils/fixtures/'));
localSettingsCache.icon = 'favicon.ico';
storage.getStorage().storagePath = path.join(__dirname, '../../../../utils/fixtures/images/');
localSettingsCache.icon = '/content/images/favicon.png';
res = {
redirect: function (statusCode) {
redirect: function (statusCode, p) {
statusCode.should.eql(302);
p.should.eql('/content/images/size/w256h256/favicon.png');
done();
}
};
@ -148,16 +89,35 @@ describe('Serve Favicon', function () {
middleware(req, res, next);
});
it('to custom favicon.png when favicon.ico is requested', function (done) {
it('custom uploaded favicon.webp', function (done) {
const middleware = serveFavicon();
req.path = '/favicon.png';
storage.getStorage().storagePath = path.join(__dirname, '../../../../utils/fixtures/images/');
localSettingsCache.icon = '/content/images/favicon.webp';
res = {
redirect: function (statusCode, p) {
statusCode.should.eql(302);
p.should.eql('/content/images/size/w256h256/format/png/favicon.webp');
done();
}
};
middleware(req, res, next);
});
it('custom uploaded favicon.ico', function (done) {
const middleware = serveFavicon();
req.path = '/favicon.ico';
configUtils.set('paths:contentPath', path.join(__dirname, '../../../../test/utils/fixtures/'));
localSettingsCache.icon = 'favicon.png';
storage.getStorage().storagePath = path.join(__dirname, '../../../../utils/fixtures/images/');
localSettingsCache.icon = '/content/images/favicon.ico';
res = {
redirect: function (statusCode) {
redirect: function (statusCode, p) {
statusCode.should.eql(302);
p.should.eql('/content/images/favicon.ico');
done();
}
};
@ -170,11 +130,12 @@ describe('Serve Favicon', function () {
req.path = '/favicon.png';
configUtils.set('paths:publicFilePath', path.join(__dirname, '../../../../test/utils/fixtures/'));
localSettingsCache.icon = '';
localSettingsCache.icon = null;
res = {
redirect: function (statusCode) {
redirect: function (statusCode, p) {
statusCode.should.eql(302);
p.should.eql('/favicon.ico');
done();
}
};

View File

@ -16,7 +16,7 @@ describe('lib/image: blog icon', function () {
}
}
}});
blogIcon.getIconUrl().should.deepEqual([{relativeUrl: '/favicon.ico'}, undefined]);
blogIcon.getIconUrl().should.deepEqual([{relativeUrl: '/content/images/2017/04/my-icon.ico'}, undefined]);
});
it('custom uploaded png blog icon', function () {
@ -29,7 +29,7 @@ describe('lib/image: blog icon', function () {
}
}
}});
blogIcon.getIconUrl().should.deepEqual([{relativeUrl: '/favicon.png'}, undefined]);
blogIcon.getIconUrl().should.deepEqual([{relativeUrl: '/content/images/size/w256h256/2017/04/my-icon.png'}, undefined]);
});
it('default ico blog icon', function () {
@ -52,7 +52,7 @@ describe('lib/image: blog icon', function () {
}
}
}});
blogIcon.getIconUrl(true).should.deepEqual([{relativeUrl: '/favicon.ico'}, true]);
blogIcon.getIconUrl(true).should.deepEqual([{relativeUrl: '/content/images/2017/04/my-icon.ico'}, true]);
});
it('custom uploaded png blog icon', function () {
@ -65,7 +65,7 @@ describe('lib/image: blog icon', function () {
}
}
}});
blogIcon.getIconUrl(true).should.deepEqual([{relativeUrl: '/favicon.png'}, true]);
blogIcon.getIconUrl(true).should.deepEqual([{relativeUrl: '/content/images/size/w256h256/2017/04/my-icon.png'}, true]);
});
it('default ico blog icon', function () {
@ -127,40 +127,6 @@ describe('lib/image: blog icon', function () {
});
});
describe('isIcoImageType', function () {
it('returns true, if icon is .ico filetype', function () {
const blogIcon = new BlogIcon({config: {}, storageUtils: {}, urlUtils: {}, settingsCache: {}});
blogIcon.isIcoImageType('icon.ico').should.be.true();
});
it('returns false, if icon is not .ico filetype', function () {
const blogIcon = new BlogIcon({config: {}, storageUtils: {}, urlUtils: {}, settingsCache: {}});
blogIcon.isIcoImageType('icon.png').should.be.false();
});
it('returns true, if icon is .ico filetype when using settingsCache', function () {
const blogIcon = new BlogIcon({config: {}, storageUtils: {}, urlUtils: {}, settingsCache: {
get: (key) => {
if (key === 'icon') {
return 'icon.ico';
}
}
}});
blogIcon.isIcoImageType().should.be.true();
});
it('returns false, if icon is not .ico filetype when using settingsCache', function () {
const blogIcon = new BlogIcon({config: {}, storageUtils: {}, urlUtils: {}, settingsCache: {
get: (key) => {
if (key === 'icon') {
return 'icon.png';
}
}
}});
blogIcon.isIcoImageType().should.be.false();
});
});
describe('getIconType', function () {
it('returns x-icon for ico icons', function () {
const blogIcon = new BlogIcon({config: {}, storageUtils: {}, urlUtils: {}, settingsCache: {}});

View File

@ -207,7 +207,7 @@ describe('lib/mobiledoc', function () {
.should.eql('<figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="/content/images/2018/04/NatGeo06.jpg" class="kg-image" alt loading="lazy" width="2000" height="1000"><figcaption>Birdies</figcaption></figure><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="/content/images/test.png" width="1000" height="500" loading="lazy" alt></div></div></div></figure>');
});
it('does not render srcsets for non-resizable images', function () {
it('does render srcsets for animated images', function () {
let mobiledoc = {
version: '0.3.1',
atoms: [],
@ -224,7 +224,27 @@ describe('lib/mobiledoc', function () {
};
mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc)
.should.eql('<figure class="kg-card kg-image-card"><img src="/content/images/2020/07/animated.gif" class="kg-image" alt loading="lazy" width="4000" height="2000"></figure>');
.should.eql('<figure class="kg-card kg-image-card"><img src="/content/images/2020/07/animated.gif" class="kg-image" alt loading="lazy" width="2000" height="1000" srcset="/content/images/size/w600/2020/07/animated.gif 600w, /content/images/size/w1000/2020/07/animated.gif 1000w, /content/images/size/w1600/2020/07/animated.gif 1600w, /content/images/size/w2400/2020/07/animated.gif 2400w" sizes="(min-width: 720px) 720px"></figure>');
});
it('does not render srcsets for non-resizable images', function () {
let mobiledoc = {
version: '0.3.1',
atoms: [],
cards: [
['image', {
cardWidth: '',
src: '/content/images/2020/07/vector.svg',
width: 4000,
height: 2000
}]
],
markups: [],
sections: [[10, 0]]
};
mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc)
.should.eql('<figure class="kg-card kg-image-card"><img src="/content/images/2020/07/vector.svg" class="kg-image" alt loading="lazy" width="4000" height="2000"></figure>');
});
it('does not render srcsets when sharp is not available', function () {

View File

@ -69,7 +69,7 @@ describe('normalize', function () {
});
});
['.gif', '.svg', '.svgz'].forEach(function (extension) {
['.svg', '.svgz'].forEach(function (extension) {
it(`should skip resizing when file extension is ${extension}`, function (done) {
req.file.ext = extension;
normalize(req, res, function () {

View File

@ -1716,10 +1716,10 @@
"@tryghost/errors" "^1.2.12"
"@tryghost/request" "^0.1.26"
"@tryghost/image-transform@1.0.32":
version "1.0.32"
resolved "https://registry.yarnpkg.com/@tryghost/image-transform/-/image-transform-1.0.32.tgz#3cf32ad931100d62306e601021c2fa8752ebe1aa"
integrity sha512-nHUfTA99BtGK1mEcC/iibs4czITbCK3OiGM4WVLQ2R/t9TAgGQYok2TBdRwAaSXH3OarTZHkWAXtwMkI6T8LqA==
"@tryghost/image-transform@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@tryghost/image-transform/-/image-transform-1.1.0.tgz#23f1abc7eca781cd65e6c99d1bfc463a744b40c1"
integrity sha512-8gSTIqPOnEBSOMc1s9xR43l8U0z7gorPVbBQWMQ6ZXM3hFAT8UubLdvqpO3OBDbsi115DRxVtH4RkkZVMHBfIQ==
dependencies:
"@tryghost/errors" "^1.2.1"
bluebird "^3.7.2"