mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-28 13:22:39 +03:00
🎨 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:
parent
f805f1637c
commit
a051ab3b69
@ -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') {
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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})
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
}
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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({
|
||||
|
@ -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`);
|
||||
|
@ -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 () {
|
||||
|
@ -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\/" \/>/);
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
@ -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: {}});
|
||||
|
@ -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 () {
|
||||
|
@ -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 () {
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user