feat: sanitize media filenames according to global slug setting (#3315)

This commit is contained in:
Stéphane Klein
2020-02-25 19:12:11 +01:00
committed by GitHub
parent 2675e1dff5
commit 8874769b31
3 changed files with 101 additions and 34 deletions

View File

@ -4,9 +4,7 @@ import { List, Map } from 'immutable';
import { insertMedia, persistMedia, deleteMedia } from '../mediaLibrary'; import { insertMedia, persistMedia, deleteMedia } from '../mediaLibrary';
jest.mock('coreSrc/backend'); jest.mock('coreSrc/backend');
jest.mock('ValueObjects/AssetProxy');
jest.mock('../waitUntil'); jest.mock('../waitUntil');
jest.mock('../../lib/urlHelper');
jest.mock('netlify-cms-lib-util', () => { jest.mock('netlify-cms-lib-util', () => {
const lib = jest.requireActual('netlify-cms-lib-util'); const lib = jest.requireActual('netlify-cms-lib-util');
return { return {
@ -62,7 +60,6 @@ describe('mediaLibrary', () => {
}); });
const { currentBackend } = require('coreSrc/backend'); const { currentBackend } = require('coreSrc/backend');
const { createAssetProxy } = require('ValueObjects/AssetProxy');
const backend = { const backend = {
persistMedia: jest.fn(() => ({ id: 'id' })), persistMedia: jest.fn(() => ({ id: 'id' })),
@ -83,12 +80,14 @@ describe('mediaLibrary', () => {
getBlobSHA.mockReturnValue('000000000000000'); getBlobSHA.mockReturnValue('000000000000000');
const { sanitizeSlug } = require('../../lib/urlHelper');
sanitizeSlug.mockReturnValue('name.png');
const store = mockStore({ const store = mockStore({
config: Map({ config: Map({
media_folder: 'static/media', media_folder: 'static/media',
slug: Map({
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
}),
}), }),
collections: Map({ collections: Map({
posts: Map({ name: 'posts' }), posts: Map({ name: 'posts' }),
@ -103,27 +102,27 @@ describe('mediaLibrary', () => {
}); });
const file = new File([''], 'name.png'); const file = new File([''], 'name.png');
const assetProxy = { path: 'static/media/name.png' };
createAssetProxy.mockReturnValue(assetProxy);
return store.dispatch(persistMedia(file)).then(() => { return store.dispatch(persistMedia(file)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions).toHaveLength(2); expect(actions).toHaveLength(2);
expect(actions[0]).toEqual({ expect(actions[0].type).toEqual('ADD_ASSET');
type: 'ADD_ASSET', expect(actions[0].payload).toEqual(
payload: { path: 'static/media/name.png' }, expect.objectContaining({
}); path: 'static/media/name.png',
expect(actions[1]).toEqual({ }),
type: 'ADD_DRAFT_ENTRY_MEDIA_FILE', );
payload: { expect(actions[1].type).toEqual('ADD_DRAFT_ENTRY_MEDIA_FILE');
expect(actions[1].payload).toEqual(
expect.objectContaining({
draft: true, draft: true,
id: '000000000000000', id: '000000000000000',
path: 'static/media/name.png', path: 'static/media/name.png',
size: file.size, size: file.size,
name: file.name, name: file.name,
}, }),
}); );
expect(getBlobSHA).toHaveBeenCalledTimes(1); expect(getBlobSHA).toHaveBeenCalledTimes(1);
expect(getBlobSHA).toHaveBeenCalledWith(file); expect(getBlobSHA).toHaveBeenCalledWith(file);
@ -135,6 +134,11 @@ describe('mediaLibrary', () => {
const store = mockStore({ const store = mockStore({
config: Map({ config: Map({
media_folder: 'static/media', media_folder: 'static/media',
slug: Map({
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
}),
}), }),
collections: Map({ collections: Map({
posts: Map({ name: 'posts' }), posts: Map({ name: 'posts' }),
@ -149,8 +153,6 @@ describe('mediaLibrary', () => {
}); });
const file = new File([''], 'name.png'); const file = new File([''], 'name.png');
const assetProxy = { path: 'static/media/name.png' };
createAssetProxy.mockReturnValue(assetProxy);
return store.dispatch(persistMedia(file)).then(() => { return store.dispatch(persistMedia(file)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
@ -159,10 +161,12 @@ describe('mediaLibrary', () => {
expect(actions).toHaveLength(3); expect(actions).toHaveLength(3);
expect(actions[0]).toEqual({ type: 'MEDIA_PERSIST_REQUEST' }); expect(actions[0]).toEqual({ type: 'MEDIA_PERSIST_REQUEST' });
expect(actions[1]).toEqual({ expect(actions[1].type).toEqual('ADD_ASSET');
type: 'ADD_ASSET', expect(actions[1].payload).toEqual(
payload: { path: 'static/media/name.png' }, expect.objectContaining({
}); path: 'static/media/name.png',
}),
);
expect(actions[2]).toEqual({ expect(actions[2]).toEqual({
type: 'MEDIA_PERSIST_SUCCESS', type: 'MEDIA_PERSIST_SUCCESS',
payload: { payload: {
@ -171,7 +175,67 @@ describe('mediaLibrary', () => {
}); });
expect(backend.persistMedia).toHaveBeenCalledTimes(1); expect(backend.persistMedia).toHaveBeenCalledTimes(1);
expect(backend.persistMedia).toHaveBeenCalledWith(store.getState().config, assetProxy); expect(backend.persistMedia).toHaveBeenCalledWith(
store.getState().config,
expect.objectContaining({
path: 'static/media/name.png',
}),
);
});
});
it('should sanitize media name if needed when persisting', () => {
const store = mockStore({
config: Map({
media_folder: 'static/media',
slug: Map({
encoding: 'ascii',
clean_accents: true,
sanitize_replacement: '_',
}),
}),
collections: Map({
posts: Map({ name: 'posts' }),
}),
integrations: Map(),
mediaLibrary: Map({
files: List(),
}),
entryDraft: Map({
entry: Map(),
}),
});
const file = new File([''], 'abc DEF éâçÖ $;, .png');
return store.dispatch(persistMedia(file)).then(() => {
const actions = store.getActions();
expect(actions).toHaveLength(3);
expect(actions[0]).toEqual({ type: 'MEDIA_PERSIST_REQUEST' });
expect(actions[1].type).toEqual('ADD_ASSET');
expect(actions[1].payload).toEqual(
expect.objectContaining({
path: 'static/media/abc_def_eaco_.png',
}),
);
expect(actions[2]).toEqual({
type: 'MEDIA_PERSIST_SUCCESS',
payload: {
file: { id: 'id' },
},
});
expect(backend.persistMedia).toHaveBeenCalledTimes(1);
expect(backend.persistMedia).toHaveBeenCalledWith(
store.getState().config,
expect.objectContaining({
path: 'static/media/abc_def_eaco_.png',
}),
);
}); });
}); });
}); });
@ -197,8 +261,6 @@ describe('mediaLibrary', () => {
}); });
const file = { name: 'name.png', id: 'id', path: 'static/media/name.png', draft: false }; const file = { name: 'name.png', id: 'id', path: 'static/media/name.png', draft: false };
const assetProxy = { path: 'static/media/name.png' };
createAssetProxy.mockReturnValue(assetProxy);
return store.dispatch(deleteMedia(file)).then(() => { return store.dispatch(deleteMedia(file)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
@ -242,8 +304,6 @@ describe('mediaLibrary', () => {
}); });
const file = { name: 'name.png', id: 'id', path: 'static/media/name.png', draft: true }; const file = { name: 'name.png', id: 'id', path: 'static/media/name.png', draft: true };
const assetProxy = { path: 'static/media/name.png' };
createAssetProxy.mockReturnValue(assetProxy);
return store.dispatch(deleteMedia(file)).then(() => { return store.dispatch(deleteMedia(file)).then(() => {
const actions = store.getActions(); const actions = store.getActions();

View File

@ -1,6 +1,6 @@
import { Map } from 'immutable'; import { Map } from 'immutable';
import { actions as notifActions } from 'redux-notifications'; import { actions as notifActions } from 'redux-notifications';
import { getBlobSHA, ImplementationMediaFile } from 'netlify-cms-lib-util'; import { basename, getBlobSHA, ImplementationMediaFile } from 'netlify-cms-lib-util';
import { currentBackend } from '../backend'; import { currentBackend } from '../backend';
import AssetProxy, { createAssetProxy } from '../valueObjects/AssetProxy'; import AssetProxy, { createAssetProxy } from '../valueObjects/AssetProxy';
import { selectIntegration } from '../reducers'; import { selectIntegration } from '../reducers';
@ -195,7 +195,7 @@ function createMediaFileFromAsset({
}): ImplementationMediaFile { }): ImplementationMediaFile {
const mediaFile = { const mediaFile = {
id, id,
name: file.name, name: basename(assetProxy.path),
displayURL: assetProxy.url, displayURL: assetProxy.url,
draft, draft,
size: file.size, size: file.size,
@ -253,7 +253,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
} catch (error) { } catch (error) {
assetProxy = createAssetProxy({ assetProxy = createAssetProxy({
file, file,
path: file.name, path: fileName,
}); });
} }
} else if (privateUpload) { } else if (privateUpload) {
@ -261,7 +261,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
} else { } else {
const entry = state.entryDraft.get('entry'); const entry = state.entryDraft.get('entry');
const collection = state.collections.get(entry?.get('collection')); const collection = state.collections.get(entry?.get('collection'));
const path = selectMediaFilePath(state.config, collection, entry, file.name, field); const path = selectMediaFilePath(state.config, collection, entry, fileName, field);
assetProxy = createAssetProxy({ assetProxy = createAssetProxy({
file, file,
path, path,
@ -278,7 +278,12 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
mediaFile = createMediaFileFromAsset({ id, file, assetProxy, draft: false }); mediaFile = createMediaFileFromAsset({ id, file, assetProxy, draft: false });
} else if (editingDraft) { } else if (editingDraft) {
const id = await getBlobSHA(file); const id = await getBlobSHA(file);
mediaFile = createMediaFileFromAsset({ id, file, assetProxy, draft: editingDraft }); mediaFile = createMediaFileFromAsset({
id,
file,
assetProxy,
draft: editingDraft,
});
return dispatch(addDraftEntryMediaFile(mediaFile)); return dispatch(addDraftEntryMediaFile(mediaFile));
} else { } else {
mediaFile = await backend.persistMedia(state.config, assetProxy); mediaFile = await backend.persistMedia(state.config, assetProxy);

View File

@ -156,7 +156,9 @@ show_preview_links: false
## Slug Type ## Slug Type
The `slug` option allows you to change how filenames for entries are created and sanitized. For modifying the actual data in a slug, see the per-collection option below. The `slug` option allows you to change how filenames for entries are created and sanitized.
It also applies to filenames of media inserted via the default media library.
For modifying the actual data in a slug, see the per-collection option below.
`slug` accepts multiple options: `slug` accepts multiple options: