fix(core): ensure against slug overwrite (#2139)
This commit is contained in:
parent
14b6292eab
commit
0ce995d78c
@ -3,7 +3,7 @@ import { actions as notifActions } from 'redux-notifications';
|
|||||||
import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
|
import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
|
||||||
import { serializeValues } from 'Lib/serializeEntryValues';
|
import { serializeValues } from 'Lib/serializeEntryValues';
|
||||||
import { currentBackend } from 'coreSrc/backend';
|
import { currentBackend } from 'coreSrc/backend';
|
||||||
import { getAsset } from 'Reducers';
|
import { getAsset, selectPublishedSlugs, selectUnpublishedSlugs } from 'Reducers';
|
||||||
import { selectFields } from 'Reducers/collections';
|
import { selectFields } from 'Reducers/collections';
|
||||||
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';
|
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';
|
||||||
import { EDITORIAL_WORKFLOW_ERROR } from 'netlify-cms-lib-util';
|
import { EDITORIAL_WORKFLOW_ERROR } from 'netlify-cms-lib-util';
|
||||||
@ -288,6 +288,9 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) {
|
|||||||
const state = getState();
|
const state = getState();
|
||||||
const entryDraft = state.entryDraft;
|
const entryDraft = state.entryDraft;
|
||||||
const fieldsErrors = entryDraft.get('fieldsErrors');
|
const fieldsErrors = entryDraft.get('fieldsErrors');
|
||||||
|
const unpublishedSlugs = selectUnpublishedSlugs(state, collection.get('name'));
|
||||||
|
const publishedSlugs = selectPublishedSlugs(state, collection.get('name'));
|
||||||
|
const usedSlugs = publishedSlugs.concat(unpublishedSlugs);
|
||||||
|
|
||||||
// Early return if draft contains validation errors
|
// Early return if draft contains validation errors
|
||||||
if (!fieldsErrors.isEmpty()) {
|
if (!fieldsErrors.isEmpty()) {
|
||||||
@ -334,6 +337,7 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) {
|
|||||||
serializedEntryDraft,
|
serializedEntryDraft,
|
||||||
assetProxies.toJS(),
|
assetProxies.toJS(),
|
||||||
state.integrations,
|
state.integrations,
|
||||||
|
usedSlugs,
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -3,7 +3,7 @@ import { actions as notifActions } from 'redux-notifications';
|
|||||||
import { serializeValues } from 'Lib/serializeEntryValues';
|
import { serializeValues } from 'Lib/serializeEntryValues';
|
||||||
import { currentBackend } from 'coreSrc/backend';
|
import { currentBackend } from 'coreSrc/backend';
|
||||||
import { getIntegrationProvider } from 'Integrations';
|
import { getIntegrationProvider } from 'Integrations';
|
||||||
import { getAsset, selectIntegration } from 'Reducers';
|
import { getAsset, selectIntegration, selectPublishedSlugs } from 'Reducers';
|
||||||
import { selectFields } from 'Reducers/collections';
|
import { selectFields } from 'Reducers/collections';
|
||||||
import { selectCollectionEntriesCursor } from 'Reducers/cursors';
|
import { selectCollectionEntriesCursor } from 'Reducers/cursors';
|
||||||
import { Cursor } from 'netlify-cms-lib-util';
|
import { Cursor } from 'netlify-cms-lib-util';
|
||||||
@ -452,6 +452,7 @@ export function persistEntry(collection) {
|
|||||||
const state = getState();
|
const state = getState();
|
||||||
const entryDraft = state.entryDraft;
|
const entryDraft = state.entryDraft;
|
||||||
const fieldsErrors = entryDraft.get('fieldsErrors');
|
const fieldsErrors = entryDraft.get('fieldsErrors');
|
||||||
|
const usedSlugs = selectPublishedSlugs(state, collection.get('name'));
|
||||||
|
|
||||||
// Early return if draft contains validation errors
|
// Early return if draft contains validation errors
|
||||||
if (!fieldsErrors.isEmpty()) {
|
if (!fieldsErrors.isEmpty()) {
|
||||||
@ -488,7 +489,14 @@ export function persistEntry(collection) {
|
|||||||
const serializedEntryDraft = entryDraft.set('entry', serializedEntry);
|
const serializedEntryDraft = entryDraft.set('entry', serializedEntry);
|
||||||
dispatch(entryPersisting(collection, serializedEntry));
|
dispatch(entryPersisting(collection, serializedEntry));
|
||||||
return backend
|
return backend
|
||||||
.persistEntry(state.config, collection, serializedEntryDraft, assetProxies.toJS())
|
.persistEntry(
|
||||||
|
state.config,
|
||||||
|
collection,
|
||||||
|
serializedEntryDraft,
|
||||||
|
assetProxies.toJS(),
|
||||||
|
state.integrations,
|
||||||
|
usedSlugs,
|
||||||
|
)
|
||||||
.then(slug => {
|
.then(slug => {
|
||||||
dispatch(
|
dispatch(
|
||||||
notifSend({
|
notifSend({
|
||||||
|
@ -17,7 +17,12 @@ import {
|
|||||||
import { createEntry } from 'ValueObjects/Entry';
|
import { createEntry } from 'ValueObjects/Entry';
|
||||||
import { sanitizeSlug } from 'Lib/urlHelper';
|
import { sanitizeSlug } from 'Lib/urlHelper';
|
||||||
import { getBackend } from 'Lib/registry';
|
import { getBackend } from 'Lib/registry';
|
||||||
import { localForage, Cursor, CURSOR_COMPATIBILITY_SYMBOL } from 'netlify-cms-lib-util';
|
import {
|
||||||
|
localForage,
|
||||||
|
Cursor,
|
||||||
|
CURSOR_COMPATIBILITY_SYMBOL,
|
||||||
|
EditorialWorkflowError,
|
||||||
|
} from 'netlify-cms-lib-util';
|
||||||
import { EDITORIAL_WORKFLOW, status } from 'Constants/publishModes';
|
import { EDITORIAL_WORKFLOW, status } from 'Constants/publishModes';
|
||||||
import {
|
import {
|
||||||
SLUG_MISSING_REQUIRED_DATE,
|
SLUG_MISSING_REQUIRED_DATE,
|
||||||
@ -256,6 +261,48 @@ class Backend {
|
|||||||
|
|
||||||
getToken = () => this.implementation.getToken();
|
getToken = () => this.implementation.getToken();
|
||||||
|
|
||||||
|
async entryExist(collection, path, slug) {
|
||||||
|
const unpublishedEntry =
|
||||||
|
this.implementation.unpublishedEntry &&
|
||||||
|
(await this.implementation.unpublishedEntry(collection, slug).catch(error => {
|
||||||
|
if (error instanceof EditorialWorkflowError && error.notUnderEditorialWorkflow) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (unpublishedEntry) return unpublishedEntry;
|
||||||
|
|
||||||
|
const publishedEntry = await this.implementation
|
||||||
|
.getEntry(collection, slug, path)
|
||||||
|
.then(({ data }) => data)
|
||||||
|
.catch(error => {
|
||||||
|
if (error.status === 404 || error.message.includes(404)) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return publishedEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateUniqueSlug(collection, entryData, slugConfig, usedSlugs) {
|
||||||
|
const slug = slugFormatter(collection, entryData, slugConfig);
|
||||||
|
const sanitizeEntrySlug = partialRight(sanitizeSlug, slugConfig);
|
||||||
|
let i = 1;
|
||||||
|
let sanitizedSlug = slug;
|
||||||
|
let uniqueSlug = sanitizedSlug;
|
||||||
|
|
||||||
|
// Check for duplicate slug in loaded entities store first before repo
|
||||||
|
while (
|
||||||
|
usedSlugs.includes(uniqueSlug) ||
|
||||||
|
(await this.entryExist(collection, selectEntryPath(collection, uniqueSlug), uniqueSlug))
|
||||||
|
) {
|
||||||
|
uniqueSlug = sanitizeEntrySlug(`${sanitizedSlug} ${i++}`);
|
||||||
|
}
|
||||||
|
return uniqueSlug;
|
||||||
|
}
|
||||||
|
|
||||||
processEntries(loadedEntries, collection) {
|
processEntries(loadedEntries, collection) {
|
||||||
const collectionFilter = collection.get('filter');
|
const collectionFilter = collection.get('filter');
|
||||||
const entries = loadedEntries.map(loadedEntry =>
|
const entries = loadedEntries.map(loadedEntry =>
|
||||||
@ -569,7 +616,15 @@ class Backend {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
persistEntry(config, collection, entryDraft, MediaFiles, integrations, options = {}) {
|
async persistEntry(
|
||||||
|
config,
|
||||||
|
collection,
|
||||||
|
entryDraft,
|
||||||
|
MediaFiles,
|
||||||
|
integrations,
|
||||||
|
usedSlugs,
|
||||||
|
options = {},
|
||||||
|
) {
|
||||||
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
|
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
|
||||||
|
|
||||||
const parsedData = {
|
const parsedData = {
|
||||||
@ -582,10 +637,11 @@ class Backend {
|
|||||||
if (!selectAllowNewEntries(collection)) {
|
if (!selectAllowNewEntries(collection)) {
|
||||||
throw new Error('Not allowed to create new entries in this collection');
|
throw new Error('Not allowed to create new entries in this collection');
|
||||||
}
|
}
|
||||||
const slug = slugFormatter(
|
const slug = await this.generateUniqueSlug(
|
||||||
collection,
|
collection,
|
||||||
entryDraft.getIn(['entry', 'data']),
|
entryDraft.getIn(['entry', 'data']),
|
||||||
config.get('slug'),
|
config.get('slug'),
|
||||||
|
usedSlugs,
|
||||||
);
|
);
|
||||||
const path = selectEntryPath(collection, slug);
|
const path = selectEntryPath(collection, slug);
|
||||||
entryObj = {
|
entryObj = {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Map, List, fromJS } from 'immutable';
|
import { Map, List, fromJS } from 'immutable';
|
||||||
|
import { startsWith } from 'lodash';
|
||||||
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';
|
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';
|
||||||
import {
|
import {
|
||||||
UNPUBLISHED_ENTRY_REQUEST,
|
UNPUBLISHED_ENTRY_REQUEST,
|
||||||
@ -140,4 +141,13 @@ export const selectUnpublishedEntriesByStatus = (state, status) => {
|
|||||||
.valueSeq();
|
.valueSeq();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const selectUnpublishedSlugs = (state, collection) => {
|
||||||
|
if (!state.get('entities')) return null;
|
||||||
|
return state
|
||||||
|
.get('entities')
|
||||||
|
.filter((v, k) => startsWith(k, `${collection}.`))
|
||||||
|
.map(entry => entry.get('slug'))
|
||||||
|
.valueSeq();
|
||||||
|
};
|
||||||
|
|
||||||
export default unpublishedEntries;
|
export default unpublishedEntries;
|
||||||
|
@ -104,8 +104,11 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
|
|||||||
export const selectEntry = (state, collection, slug) =>
|
export const selectEntry = (state, collection, slug) =>
|
||||||
state.getIn(['entities', `${collection}.${slug}`]);
|
state.getIn(['entities', `${collection}.${slug}`]);
|
||||||
|
|
||||||
|
export const selectPublishedSlugs = (state, collection) =>
|
||||||
|
state.getIn(['pages', collection, 'ids'], List());
|
||||||
|
|
||||||
export const selectEntries = (state, collection) => {
|
export const selectEntries = (state, collection) => {
|
||||||
const slugs = state.getIn(['pages', collection, 'ids']);
|
const slugs = selectPublishedSlugs(state, collection);
|
||||||
return slugs && slugs.map(slug => selectEntry(state, collection, slug));
|
return slugs && slugs.map(slug => selectEntry(state, collection, slug));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -39,6 +39,9 @@ export const selectEntry = (state, collection, slug) =>
|
|||||||
export const selectEntries = (state, collection) =>
|
export const selectEntries = (state, collection) =>
|
||||||
fromEntries.selectEntries(state.entries, collection);
|
fromEntries.selectEntries(state.entries, collection);
|
||||||
|
|
||||||
|
export const selectPublishedSlugs = (state, collection) =>
|
||||||
|
fromEntries.selectPublishedSlugs(state.entries, collection);
|
||||||
|
|
||||||
export const selectSearchedEntries = state => {
|
export const selectSearchedEntries = state => {
|
||||||
const searchItems = state.search.get('entryIds');
|
const searchItems = state.search.get('entryIds');
|
||||||
return (
|
return (
|
||||||
@ -58,6 +61,9 @@ export const selectUnpublishedEntry = (state, collection, slug) =>
|
|||||||
export const selectUnpublishedEntriesByStatus = (state, status) =>
|
export const selectUnpublishedEntriesByStatus = (state, status) =>
|
||||||
fromEditorialWorkflow.selectUnpublishedEntriesByStatus(state.editorialWorkflow, status);
|
fromEditorialWorkflow.selectUnpublishedEntriesByStatus(state.editorialWorkflow, status);
|
||||||
|
|
||||||
|
export const selectUnpublishedSlugs = (state, collection) =>
|
||||||
|
fromEditorialWorkflow.selectUnpublishedSlugs(state.editorialWorkflow, collection);
|
||||||
|
|
||||||
export const selectIntegration = (state, collection, hook) =>
|
export const selectIntegration = (state, collection, hook) =>
|
||||||
fromIntegrations.selectIntegration(state.integrations, collection, hook);
|
fromIntegrations.selectIntegration(state.integrations, collection, hook);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user