Skip to content

Commit

Permalink
Simplified loader which does not depend on 'config' (not to be used a…
Browse files Browse the repository at this point in the history
…ccording to Vercel: vercel/next.js#35115). Added whitelisting config for default next/image sizes.
  • Loading branch information
ambrauer committed Apr 5, 2022
1 parent 6aec000 commit 55a7308
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,35 @@ const StyleguideFieldUsageImage = (props: StyleguideFieldUsageImageProps): JSX.E
{/*
Advanced image usage example
editable: controls whether image can be edited in Sitecore Experience Editor
imageParams: parameters that are passed to Sitecore to perform server-side resizing of the image.
Sample rescales image to max 100x50 dimensions on the server, respecting aspect ratio
IMPORTANT: imageParams must be whitelisted for resizing to occur. See /sitecore/config/*.config (search for 'allowedMediaParams')
unoptimized: disables next/image source optimization in favor of imageParams
imageParams: parameters that are passed to Sitecore to perform server-side resizing of the image
Sample rescales image to max 100x50 dimensions on the server, respecting aspect ratio
IMPORTANT: imageParams must be whitelisted for resizing to occur. See /sitecore/config/*.config (search for 'allowedMediaParams')
any other attributes: pass through to img tag
*/}
<p>Advanced image (not editable)</p>
<NextImage
field={props.fields.sample2}
editable={false}
unoptimized={true}
imageParams={{ mw: 100, mh: 50 }}
height="50"
width="94"
data-sample="other-attributes-pass-through"
/>

{/*
srcSet in Nextjs Image is set inside of the next.config by setting an array of deviceSizes and imageSizes inside the images option.
IMPORTANT: These sizes should match your Sitecore server-side allowlist. See /sitecore/config/*.config (search for 'allowedMediaParams')
next/image generates responsive srcSet automatically based on layout. See https://nextjs.org/docs/api-reference/next/image#layout.
IMPORTANT: The generated sizes should match your Sitecore server-side allowlist. See /sitecore/config/*.config (search for 'allowedMediaParams')
*/}
<p>Srcset responsive image</p>
<NextImage field={props.fields.sample2} height="105" width="200" layout="responsive" />
<NextImage
field={props.fields.sample2}
height="105"
width="200"
sizes="50vw"
layout="responsive"
/>
</StyleguideSpecimen>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Field } from '@sitecore-jss/sitecore-jss-nextjs';
/**
* Shared styleguide specimen fields
*/
export type StyleguideSpecimenFields = {
export type StyleguideSpecimenFields = {
fields: {
description: Field<string>;
heading: Field<string>;
Expand Down
13 changes: 0 additions & 13 deletions packages/create-sitecore-jss/src/templates/nextjs/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,6 @@ const nextConfig = {
// Enable React Strict Mode
reactStrictMode: true,

images: {
// We use a custom loader function in the NextImage component (passed to next/image).
// See: https://nextjs.org/docs/api-reference/next/image#loader
// The config here is more of a vanity configuration as it does not affect the functionality, but is recommended by Vercel.
loader: 'custom',
// IMPORTANT: 'path' is required as this drives our custom loader when media URLs are relative.
path: jssConfig.sitecoreApiHost,
// These widths are used when the next/image component uses layout="responsive" or layout="fill" to ensure the correct image is served for user's device.
// It is used to generate the srcset attribute for the image, using two sizes 300 and 100px max widths, respecting aspect ratio.
// IMPORTANT: These sizes should match your Sitecore server-side allowlist. See /sitecore/config/*.config (search for 'allowedMediaParams')
deviceSizes: [100, 300],
},

async rewrites() {
// When in connected mode we want to proxy Sitecore paths off to Sitecore
return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
</apps>
<!--
IMAGE RESIZING WHITELIST
Using Sitecore server-side media resizing (i.e. the `imageParams` or `srcSet` props on the `<Image/>` helper component)
Using Sitecore server-side media resizing (i.e. the `imageParams` or `srcSet` props on the `<Image/>` helper component or the `<NextImage/>` helper component)
could expose your Sitecore server to a denial-of-service attack by rescaling an image with many arbitrary dimensions.
In JSS resizing param sets that are unknown are rejected by a whitelist.

Expand All @@ -115,10 +115,24 @@
<styleguide-image-sample>
mw=100,mh=50
</styleguide-image-sample>
<styleguide-image-sample-adaptive>
mw=300
mw=100
</styleguide-image-sample-adaptive>
<next-image-default>
mw=16
mw=32
mw=48
mw=64
mw=96
mw=128
mw=256
mw=384
mw=640
mw=750
mw=828
mw=1080
mw=1200
mw=1920
mw=2048
mw=3840
</next-image-default>
</allowedMediaParams>
</javaScriptServices>
<!--
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ export const getServerSideProps: GetServerSideProps = async (context) => {

return {
props,
<% if (prerender === 'SSG') { -%>
<% if (prerender === 'SSG') { -%>
// Next.js will attempt to re-generate the page:
// - When a request comes in
// - At most once every 5 seconds
revalidate: 5, // In seconds
<% } -%>
<% } -%>
notFound: props.notFound, // Returns custom 404 page with a status code of 404 when true
};
};
Expand Down
132 changes: 41 additions & 91 deletions packages/sitecore-jss-nextjs/src/components/NextImage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import chai, { use } from 'chai';
import chaiString from 'chai-string';
import { mount } from 'enzyme';
import React from 'react';
import { NextImage, loader } from './NextImage';
import { NextImage, sitecoreLoader } from './NextImage';
import { ImageField } from '@sitecore-jss/sitecore-jss-react';
import { ImageLoader, ImageLoaderProps } from 'next/image';
import { spy, match } from 'sinon';
Expand All @@ -12,98 +12,48 @@ import { SinonSpy } from 'sinon';

use(sinonChai);
const expect = chai.use(chaiString).expect;
describe('Next Loader function', () => {
it('should append configPath and query string params when src is relative', () => {
const params: ImageLoaderProps = {
config: {
deviceSizes: [],
imageSizes: [],
path: 'https://cm.jss.localhost',
allSizes: [],
loader: 'default',
domains: [],
disableStaticImages: false,
minimumCacheTTL: 1,
formats: [],
dangerouslyAllowSVG: false,
contentSecurityPolicy: 'test',
},
src: '/assets/img/test0.png',
width: 100,
};
const result = loader(params);
expect(result).to.be.a('string');
expect(result).to.equal(`${params.config.path}/assets/img/test0.png?mw=100`);
});

it('should not require config path when src is absolute', () => {
const params: ImageLoaderProps = {
config: {
deviceSizes: [],
imageSizes: [],
path: undefined,
allSizes: [],
loader: 'default',
domains: [],
disableStaticImages: false,
minimumCacheTTL: 1,
formats: [],
dangerouslyAllowSVG: false,
contentSecurityPolicy: 'test',
},
src: 'https://cm.jss.localhost/assets/img/test0.png',
width: 100,
};
const result = loader(params);
expect(result).to.be.a('string');
expect(result).to.equal('https://cm.jss.localhost/assets/img/test0.png?mw=100');
});
describe('sitecoreLoader', () => {
[
{
description: 'relative URL',
root: '/assets/img/test0.png',
},
{
description: 'absolute URL',
root: 'https://cm.jss.localhost/assets/img/test0.png',
},
].forEach((value) => {
describe(value.description, () => {
it('should append mw query string param', () => {
const params: Partial<ImageLoaderProps> = {
src: value.root,
width: 100,
};
const result = sitecoreLoader(params as ImageLoaderProps);
expect(result).to.be.a('string');
expect(result).to.equal(`${value.root}?mw=100`);
});

it('should not append config path when src is absolute', () => {
const params: ImageLoaderProps = {
config: {
deviceSizes: [],
imageSizes: [],
path: 'https://cm.jss.localhost/',
allSizes: [],
loader: 'default',
domains: [],
disableStaticImages: false,
minimumCacheTTL: 1,
formats: [],
dangerouslyAllowSVG: false,
contentSecurityPolicy: 'test',
},
src: 'https://cm.jss.localhost/assets/img/test0.png',
width: 100,
};
const result = loader(params);
console.log(result);
expect(result).to.be.a('string');
expect(result).to.equal('https://cm.jss.localhost/assets/img/test0.png?mw=100');
});
it('should override existing mw query string param', () => {
const params: Partial<ImageLoaderProps> = {
src: `${value.root}?mw=400`,
width: 100,
};
const result = sitecoreLoader(params as ImageLoaderProps);
expect(result).to.be.a('string');
expect(result).to.equal(`${value.root}?mw=100`);
});

it('should throw an error if path is not configured', () => {
const params: ImageLoaderProps = {
config: {
deviceSizes: [],
imageSizes: [],
path: undefined,
allSizes: [],
loader: 'default',
domains: [],
disableStaticImages: false,
minimumCacheTTL: 1,
formats: [],
dangerouslyAllowSVG: false,
contentSecurityPolicy: 'test',
},
src: '/assets/img/test0.png?mw=100',
width: 100,
};
expect(() => loader(params)).to.throw(
'Failed to load image. Please make sure images path is configured correctly in next.config.js'
);
it('should preserve existing query string params', () => {
const params: Partial<ImageLoaderProps> = {
src: `${value.root}?mw=400&mh=100&q=50`,
width: 100,
};
const result = sitecoreLoader(params as ImageLoaderProps);
expect(result).to.be.a('string');
expect(result).to.equal(`${value.root}?mw=100&mh=100&q=50`);
});
});
});
});

Expand Down
22 changes: 7 additions & 15 deletions packages/sitecore-jss-nextjs/src/components/NextImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,11 @@ import Image, {

type NextImageProps = Omit<ImageProps, 'media'> & Partial<NextImageProperties>;

export const loader: ImageLoader = ({ config, src, width }: ImageLoaderProps): string => {
try {
const r = /^(?:[a-z]+:)?\/\//i;
const url = r.test(src) ? new URL(`${src}`) : new URL(`${config.path}${src}`);
const params = url.searchParams;
params.set('mw', params.get('mw') || width.toString());
params.delete('w');
return url.href;
} catch (err) {
throw new Error(
'Failed to load image. Please make sure images path is configured correctly in next.config.js'
);
}
export const sitecoreLoader: ImageLoader = ({ src, width }: ImageLoaderProps): string => {
const [root, paramString] = src.split('?');
const params = new URLSearchParams(paramString);
params.set('mw', width.toString());
return `${root}?${params}`;
};

export const NextImage: React.SFC<NextImageProps> = ({
Expand Down Expand Up @@ -82,10 +74,10 @@ export const NextImage: React.SFC<NextImageProps> = ({
),
};

const customLoader = (otherProps.loader ? otherProps.loader : loader) as ImageLoader;
const loader = (otherProps.loader ? otherProps.loader : sitecoreLoader) as ImageLoader;

if (attrs) {
return <Image loader={customLoader} {...attrs} />;
return <Image loader={loader} {...attrs} />;
}

return null; // we can't handle the truth
Expand Down

0 comments on commit 55a7308

Please sign in to comment.