Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions packages/content/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"@types/proxyquire": "^1.3.31",
"@types/sinon": "^17.0.4",
"@types/sinon-chai": "^4.0.0",
"@types/url-parse": "1.4.11",
"@typescript-eslint/eslint-plugin": "8.39.0",
"@typescript-eslint/parser": "8.39.0",
"chai": "^4.4.1",
Expand Down Expand Up @@ -81,8 +80,7 @@
"chalk": "^4.1.2",
"debug": "^4.4.0",
"glob": "^11.0.2",
"graphql": "^16.11.0",
"url-parse": "^1.5.10"
"graphql": "^16.11.0"
},
"description": "",
"types": "types/index.d.ts",
Expand Down
59 changes: 31 additions & 28 deletions packages/content/src/media/media-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
// what is `import x = require('x');`? great question: https://github.com/Microsoft/TypeScript/issues/5073
import chai from 'chai';
import chaiString from 'chai-string';
import URL from 'url-parse';
import { replaceMediaUrlPrefix, getSrcSet, updateImageUrl, getRequiredParams } from './media-api';

const expect = chai.use(chaiString).expect;
const parseUrl = (input: string) => new URL(input, 'https://content-sdk.invalid');

describe('getRequiredParams', () => {
it('should return required query string params', () => {
Expand Down Expand Up @@ -43,48 +43,48 @@ describe('updateImageUrl', () => {
it('should override parameters with those provided', () => {
const original = 'http://sitecore/-/media/lorem.png?mh=3&mw=4';
const updated = updateImageUrl(original, { mh: '5', mw: '6' });
const url = URL(updated, true);
expect(url.query.mh).to.equal('5');
expect(url.query.mw).to.equal('6');
const url = parseUrl(updated);
expect(url.searchParams.get('mh')).to.equal('5');
expect(url.searchParams.get('mw')).to.equal('6');
});

it('should remove non-required query parameters not provided', () => {
const original =
'http://sitecore/-/media/lorem.png?h=1&w=2&mh=3&mw=4&hash=CC5043DC03C6C27F40EDB08CF84AB8670C05D63D';
const updated = updateImageUrl(original, { mh: '5', mw: '6' });
const url = URL(updated, true);
expect(url.query.mh).to.equal('5');
expect(url.query.mw).to.equal('6');
expect(url.query.h).to.be.undefined;
expect(url.query.w).to.be.undefined;
expect(url.query.hash).to.be.undefined;
const url = parseUrl(updated);
expect(url.searchParams.get('mh')).to.equal('5');
expect(url.searchParams.get('mw')).to.equal('6');
expect(url.searchParams.get('h')).to.be.null;
expect(url.searchParams.get('w')).to.be.null;
expect(url.searchParams.get('hash')).to.be.null;
});

it('should preserve required query parameters', () => {
const original =
'http://sitecore/-/media/lorem.png?rev=100&db=master&la=en&vs=200&ts=foo&mh=3&mw=4';
const updated = updateImageUrl(original, { mh: '5', mw: '6' });
const url = URL(updated, true);
expect(url.query.mh).to.equal('5');
expect(url.query.mw).to.equal('6');
expect(url.query.rev).to.equal('100');
expect(url.query.db).to.equal('master');
expect(url.query.la).to.equal('en');
expect(url.query.vs).to.equal('200');
expect(url.query.ts).to.equal('foo');
const url = parseUrl(updated);
expect(url.searchParams.get('mh')).to.equal('5');
expect(url.searchParams.get('mw')).to.equal('6');
expect(url.searchParams.get('rev')).to.equal('100');
expect(url.searchParams.get('db')).to.equal('master');
expect(url.searchParams.get('la')).to.equal('en');
expect(url.searchParams.get('vs')).to.equal('200');
expect(url.searchParams.get('ts')).to.equal('foo');
});

it('should replace /-/media/ with /-/jssmedia/', () => {
const original = 'http://sitecore/-/media/lorem/ipsum.jpg';
const updated = updateImageUrl(original, { foo: 'bar' });
const url = URL(updated);
const url = parseUrl(updated);
expect(url.pathname).to.startsWith('/-/jssmedia/');
});

it('should replace /~/media/ with /~/jssmedia/', () => {
const original = 'http://sitecore/~/media/lorem/ipsum.jpg';
const updated = updateImageUrl(original, { foo: 'bar' });
const url = URL(updated);
const url = parseUrl(updated);
expect(url.pathname).to.startsWith('/~/jssmedia/');
});

Expand All @@ -93,31 +93,31 @@ describe('updateImageUrl', () => {
const original = 'http://sitecore/-assets/lorem/ipsum.jpg';
const mediaUrlPrefix = /\/([-~]{1})assets\//i;
const updated = updateImageUrl(original, { foo: 'bar' }, mediaUrlPrefix);
const url = URL(updated);
const url = parseUrl(updated);
expect(url.pathname).to.startsWith('/-/jssmedia/');
});

it('should replace /~assets/ with /~/jssmedia', () => {
const original = 'http://sitecore/~assets/lorem/ipsum.jpg';
const mediaUrlPrefix = /\/([-~]{1})assets\//i;
const updated = updateImageUrl(original, { foo: 'bar' }, mediaUrlPrefix);
const url = URL(updated);
const url = parseUrl(updated);
expect(url.pathname).to.startsWith('/~/jssmedia/');
});

it('should replace /-/assets/ with /-/jssmedia/', () => {
const original = 'http://sitecore/-/assets/lorem/ipsum.jpg';
const mediaUrlPrefix = /\/([-~]{1})\/assets\//i;
const updated = updateImageUrl(original, { foo: 'bar' }, mediaUrlPrefix);
const url = URL(updated);
const url = parseUrl(updated);
expect(url.pathname).to.startsWith('/-/jssmedia/');
});

it('should replace /~/assets/ with /~/jssmedia/', () => {
const original = 'http://sitecore/~/assets/lorem/ipsum.jpg';
const mediaUrlPrefix = /\/([-~]{1})\/assets\//i;
const updated = updateImageUrl(original, { foo: 'bar' }, mediaUrlPrefix);
const url = URL(updated);
const url = parseUrl(updated);
expect(url.pathname).to.startsWith('/~/jssmedia/');
});
});
Expand All @@ -127,12 +127,15 @@ describe('updateImageUrl', () => {
'/media/lorem/ipsum.jpg?x=valueX&y=value111&rev=109010&db=333&la=444&vs=555&ts=666&unknownParam=54321';
const params = { y: 'valueY', z: 'valueZ' };
const parsed = updateImageUrl(src, params);
const url = URL(parsed, {}, true);
const url = parseUrl(parsed);

expect(url.toString()).equal(
'https://content-sdk.invalid/media/lorem/ipsum.jpg?y=valueY&z=valueZ&rev=109010&db=333&la=444&vs=555&ts=666'
);
expect(parsed).equal(
'/media/lorem/ipsum.jpg?y=valueY&z=valueZ&rev=109010&db=333&la=444&vs=555&ts=666'
);
expect(url.query).deep.equal({
expect(Object.fromEntries(url.searchParams.entries())).deep.equal({
y: 'valueY',
z: 'valueZ',
rev: '109010',
Expand Down Expand Up @@ -211,14 +214,14 @@ describe('getSrcSet', () => {
it('should replace /-/media/ with /-/jssmedia/', () => {
const original = 'http://sitecore/-/media/lorem/ipsum.jpg';
const updated = replaceMediaUrlPrefix(original);
const url = URL(updated);
const url = parseUrl(updated);
expect(url.pathname).to.startsWith('/-/jssmedia/');
});

it('should replace /~/media/ with /~/jssmedia/', () => {
const original = 'http://sitecore/~/media/lorem/ipsum.jpg';
const updated = replaceMediaUrlPrefix(original);
const url = URL(updated);
const url = parseUrl(updated);
expect(url.pathname).to.startsWith('/~/jssmedia/');
});
});
Expand Down
90 changes: 72 additions & 18 deletions packages/content/src/media/media-api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,56 @@
import URL from 'url-parse';

// finds the Sitecore media URL prefix
const mediaUrlPrefixRegex = /\/([-~]{1})\/media\//i;
const internalUrlBase = 'https://content-sdk.invalid';
const absoluteUrlRegex = /^[a-zA-Z][a-zA-Z\d+.-]*:/;

type ParsedMediaUrlKind = 'absolute' | 'protocol-relative' | 'root-relative' | 'relative-path';

type ParsedMediaUrl = {
kind: ParsedMediaUrlKind;
url: URL;
};

/**
* Parses a media URL while preserving whether the original input was absolute or relative.
* @param {string} input The media URL to parse
* @returns {ParsedMediaUrl} The parsed URL and original input kind
*/
const parseMediaUrl = (input: string): ParsedMediaUrl => {
let kind: ParsedMediaUrlKind;

if (absoluteUrlRegex.test(input)) {
kind = 'absolute';
} else if (input.startsWith('//')) {
kind = 'protocol-relative';
} else if (input.startsWith('/')) {
kind = 'root-relative';
} else {
kind = 'relative-path';
}

return {
kind,
url: kind === 'absolute' ? new URL(input) : new URL(input, internalUrlBase),
};
};

/**
* Formats a parsed media URL back to the same absolute or relative form as the original input.
* @param {ParsedMediaUrl} parsed The parsed media URL details
* @returns {string} The formatted URL
*/
const formatMediaUrl = ({ kind, url }: ParsedMediaUrl): string => {
switch (kind) {
case 'absolute':
return url.toString();
case 'protocol-relative':
return `//${url.host}${url.pathname}${url.search}${url.hash}`;
case 'root-relative':
return `${url.pathname}${url.search}${url.hash}`;
default:
return `${url.pathname.replace(/^\//, '')}${url.search}${url.hash}`;
}
};

/**
* Get required query string params which should be merged with user params
Expand All @@ -27,15 +76,15 @@ export const replaceMediaUrlPrefix = (
url: string,
mediaUrlPrefix: RegExp = mediaUrlPrefixRegex
): string => {
const parsed = URL(url, {}, true);
const parsed = parseMediaUrl(url);

const match = mediaUrlPrefix.exec(parsed.pathname);
const match = mediaUrlPrefix.exec(parsed.url.pathname);
if (match && match.length > 1) {
// regex will provide us with /-/ or /~/ type
parsed.set('pathname', parsed.pathname.replace(mediaUrlPrefix, `/${match[1]}/jssmedia/`));
parsed.url.pathname = parsed.url.pathname.replace(mediaUrlPrefix, `/${match[1]}/jssmedia/`);
}

return parsed.toString();
return formatMediaUrl(parsed);
};

/**
Expand All @@ -59,26 +108,31 @@ export const updateImageUrl = (
// if params aren't supplied, no need to run it through Content SDK media handler
return url;
}
// polyfill node `global` in browser to workaround https://github.com/unshiftio/url-parse/issues/150
if (typeof window !== 'undefined' && !window.global) {
window.global = {} as typeof globalThis;
}

const parsed = URL(replaceMediaUrlPrefix(url, mediaUrlPrefix), {}, true);

const requiredParams = getRequiredParams(parsed.query);
const parsed = parseMediaUrl(replaceMediaUrlPrefix(url, mediaUrlPrefix));
const requiredParams = getRequiredParams({
rev: parsed.url.searchParams.get('rev') || undefined,
db: parsed.url.searchParams.get('db') || undefined,
la: parsed.url.searchParams.get('la') || undefined,
vs: parsed.url.searchParams.get('vs') || undefined,
ts: parsed.url.searchParams.get('ts') || undefined,
});
const query = new URLSearchParams();

const query = { ...params };
Object.entries(params).forEach(([key, param]) => {
if (param !== undefined) {
query.set(key, `${param}`);
}
});

Object.entries(requiredParams).forEach(([key, param]) => {
if (param) {
query[key] = param;
query.set(key, param);
}
});

parsed.set('query', query);
parsed.url.search = query.toString();

return parsed.toString();
return formatMediaUrl(parsed);
};

/**
Expand Down
Loading