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 { serializeValues } from 'Lib/serializeEntryValues';
|
||||
import { currentBackend } from 'coreSrc/backend';
|
||||
import { getAsset } from 'Reducers';
|
||||
import { getAsset, selectPublishedSlugs, selectUnpublishedSlugs } from 'Reducers';
|
||||
import { selectFields } from 'Reducers/collections';
|
||||
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';
|
||||
import { EDITORIAL_WORKFLOW_ERROR } from 'netlify-cms-lib-util';
|
||||
@ -288,6 +288,9 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) {
|
||||
const state = getState();
|
||||
const entryDraft = state.entryDraft;
|
||||
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
|
||||
if (!fieldsErrors.isEmpty()) {
|
||||
@ -334,6 +337,7 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) {
|
||||
serializedEntryDraft,
|
||||
assetProxies.toJS(),
|
||||
state.integrations,
|
||||
usedSlugs,
|
||||
];
|
||||
|
||||
try {
|
||||
|
@ -3,7 +3,7 @@ import { actions as notifActions } from 'redux-notifications';
|
||||
import { serializeValues } from 'Lib/serializeEntryValues';
|
||||
import { currentBackend } from 'coreSrc/backend';
|
||||
import { getIntegrationProvider } from 'Integrations';
|
||||
import { getAsset, selectIntegration } from 'Reducers';
|
||||
import { getAsset, selectIntegration, selectPublishedSlugs } from 'Reducers';
|
||||
import { selectFields } from 'Reducers/collections';
|
||||
import { selectCollectionEntriesCursor } from 'Reducers/cursors';
|
||||
import { Cursor } from 'netlify-cms-lib-util';
|
||||
@ -452,6 +452,7 @@ export function persistEntry(collection) {
|
||||
const state = getState();
|
||||
const entryDraft = state.entryDraft;
|
||||
const fieldsErrors = entryDraft.get('fieldsErrors');
|
||||
const usedSlugs = selectPublishedSlugs(state, collection.get('name'));
|
||||
|
||||
// Early return if draft contains validation errors
|
||||
if (!fieldsErrors.isEmpty()) {
|
||||
@ -488,7 +489,14 @@ export function persistEntry(collection) {
|
||||
const serializedEntryDraft = entryDraft.set('entry', serializedEntry);
|
||||
dispatch(entryPersisting(collection, serializedEntry));
|
||||
return backend
|
||||
.persistEntry(state.config, collection, serializedEntryDraft, assetProxies.toJS())
|
||||
.persistEntry(
|
||||
state.config,
|
||||
collection,
|
||||
serializedEntryDraft,
|
||||
assetProxies.toJS(),
|
||||
state.integrations,
|
||||
usedSlugs,
|
||||
)
|
||||
.then(slug => {
|
||||
dispatch(
|
||||
notifSend({
|
||||
|
@ -17,7 +17,12 @@ import {
|
||||
import { createEntry } from 'ValueObjects/Entry';
|
||||
import { sanitizeSlug } from 'Lib/urlHelper';
|
||||
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 {
|
||||
SLUG_MISSING_REQUIRED_DATE,
|
||||
@ -256,6 +261,48 @@ class Backend {
|
||||
|
||||
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) {
|
||||
const collectionFilter = collection.get('filter');
|
||||
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 parsedData = {
|
||||
@ -582,10 +637,11 @@ class Backend {
|
||||
if (!selectAllowNewEntries(collection)) {
|
||||
throw new Error('Not allowed to create new entries in this collection');
|
||||
}
|
||||
const slug = slugFormatter(
|
||||
const slug = await this.generateUniqueSlug(
|
||||
collection,
|
||||
entryDraft.getIn(['entry', 'data']),
|
||||
config.get('slug'),
|
||||
usedSlugs,
|
||||
);
|
||||
const path = selectEntryPath(collection, slug);
|
||||
entryObj = {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Map, List, fromJS } from 'immutable';
|
||||
import { startsWith } from 'lodash';
|
||||
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';
|
||||
import {
|
||||
UNPUBLISHED_ENTRY_REQUEST,
|
||||
@ -140,4 +141,13 @@ export const selectUnpublishedEntriesByStatus = (state, status) => {
|
||||
.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;
|
||||
|
@ -104,8 +104,11 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
|
||||
export const selectEntry = (state, collection, slug) =>
|
||||
state.getIn(['entities', `${collection}.${slug}`]);
|
||||
|
||||
export const selectPublishedSlugs = (state, collection) =>
|
||||
state.getIn(['pages', collection, 'ids'], List());
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
|
@ -39,6 +39,9 @@ export const selectEntry = (state, collection, slug) =>
|
||||
export const selectEntries = (state, collection) =>
|
||||
fromEntries.selectEntries(state.entries, collection);
|
||||
|
||||
export const selectPublishedSlugs = (state, collection) =>
|
||||
fromEntries.selectPublishedSlugs(state.entries, collection);
|
||||
|
||||
export const selectSearchedEntries = state => {
|
||||
const searchItems = state.search.get('entryIds');
|
||||
return (
|
||||
@ -58,6 +61,9 @@ export const selectUnpublishedEntry = (state, collection, slug) =>
|
||||
export const selectUnpublishedEntriesByStatus = (state, status) =>
|
||||
fromEditorialWorkflow.selectUnpublishedEntriesByStatus(state.editorialWorkflow, status);
|
||||
|
||||
export const selectUnpublishedSlugs = (state, collection) =>
|
||||
fromEditorialWorkflow.selectUnpublishedSlugs(state.editorialWorkflow, collection);
|
||||
|
||||
export const selectIntegration = (state, collection, hook) =>
|
||||
fromIntegrations.selectIntegration(state.integrations, collection, hook);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user