diff --git a/packages/core/strapi/lib/core-api/controller/collection-type.js b/packages/core/strapi/lib/core-api/controller/collection-type.js index 57828588fe..0e95923eab 100644 --- a/packages/core/strapi/lib/core-api/controller/collection-type.js +++ b/packages/core/strapi/lib/core-api/controller/collection-type.js @@ -22,7 +22,6 @@ const createCollectionTypeController = ({ contentType }) => { const sanitizedQuery = await this.sanitizeQuery(ctx); const { results, pagination } = await strapi.service(uid).find(sanitizedQuery); const sanitizedResults = await this.sanitizeOutput(results, ctx); - return this.transformResponse(sanitizedResults, { pagination }); }, diff --git a/packages/core/strapi/lib/core-api/controller/index.js b/packages/core/strapi/lib/core-api/controller/index.js index f30fa8e3b6..bc9c0cde78 100644 --- a/packages/core/strapi/lib/core-api/controller/index.js +++ b/packages/core/strapi/lib/core-api/controller/index.js @@ -18,7 +18,7 @@ const createController = ({ contentType }) => { return transformResponse(data, meta, { contentType }); }, - sanitizeOutput(data, ctx) { + async sanitizeOutput(data, ctx) { const auth = getAuthFromKoaContext(ctx); return sanitize.contentAPI.output(data, contentType, { auth }); diff --git a/packages/core/utils/lib/async.js b/packages/core/utils/lib/async.js index e5dd3dbe1b..c76a0de638 100644 --- a/packages/core/utils/lib/async.js +++ b/packages/core/utils/lib/async.js @@ -6,9 +6,8 @@ const { curry, curryN } = require('lodash/fp'); function pipeAsync(...methods) { return async (data) => { let res = data; - - for (const method of methods) { - res = await method(res); + for (let i = 0; i < methods.length; i += 1) { + res = await methods[i](res); } return res; diff --git a/packages/core/utils/lib/sanitize/index.js b/packages/core/utils/lib/sanitize/index.js index 6bb2b8f28e..babd4753eb 100644 --- a/packages/core/utils/lib/sanitize/index.js +++ b/packages/core/utils/lib/sanitize/index.js @@ -37,12 +37,16 @@ const createContentAPISanitizers = () => { return pipeAsync(...transforms)(data); }; - const sanitizeOuput = (data, schema, { auth } = {}) => { + const sanitizeOutput = async (data, schema, { auth } = {}) => { if (isArray(data)) { - return Promise.all(data.map((entry) => sanitizeOuput(entry, schema, { auth }))); + const res = new Array(data.length); + for (let i = 0; i < data.length; i += 1) { + res[i] = await sanitizeOutput(data[i], schema, { auth }); + } + return res; } - const transforms = [sanitizers.defaultSanitizeOutput(schema)]; + const transforms = [(data) => sanitizers.defaultSanitizeOutput(schema, data)]; if (auth) { transforms.push(traverseEntity(visitors.removeRestrictedRelations(auth), { schema })); @@ -122,7 +126,7 @@ const createContentAPISanitizers = () => { return { input: sanitizeInput, - output: sanitizeOuput, + output: sanitizeOutput, query: sanitizeQuery, filters: sanitizeFilters, sort: sanitizeSort, diff --git a/packages/core/utils/lib/sanitize/sanitizers.js b/packages/core/utils/lib/sanitize/sanitizers.js index b87ced2400..67ab23f380 100644 --- a/packages/core/utils/lib/sanitize/sanitizers.js +++ b/packages/core/utils/lib/sanitize/sanitizers.js @@ -20,17 +20,20 @@ const { removeMorphToRelations, } = require('./visitors'); -const sanitizePasswords = curry((schema, entity) => { +const sanitizePasswords = (schema) => async (entity) => { return traverseEntity(removePassword, { schema }, entity); -}); +}; -const sanitizePrivates = curry((schema, entity) => { - return traverseEntity(removePrivate, { schema }, entity); -}); - -const defaultSanitizeOutput = curry((schema, entity) => { - return pipeAsync(sanitizePrivates(schema), sanitizePasswords(schema))(entity); -}); +const defaultSanitizeOutput = async (schema, entity) => { + return traverseEntity( + (...args) => { + removePassword(...args); + removePrivate(...args); + }, + { schema }, + entity + ); +}; const defaultSanitizeFilters = curry((schema, filters) => { return pipeAsync( @@ -140,7 +143,6 @@ const defaultSanitizePopulate = curry((schema, populate) => { module.exports = { sanitizePasswords, - sanitizePrivates, defaultSanitizeOutput, defaultSanitizeFilters, defaultSanitizeSort, diff --git a/packages/core/utils/lib/traverse-entity.js b/packages/core/utils/lib/traverse-entity.js index 7962cca2ac..409c34bfcd 100644 --- a/packages/core/utils/lib/traverse-entity.js +++ b/packages/core/utils/lib/traverse-entity.js @@ -1,6 +1,42 @@ 'use strict'; -const { cloneDeep, isObject, isArray, isNil, curry } = require('lodash/fp'); +const { clone, isObject, isArray, isNil, curry } = require('lodash/fp'); + +const traverseMorphRelationTarget = async (visitor, path, entry) => { + const targetSchema = strapi.getModel(entry.__type); + + const traverseOptions = { schema: targetSchema, path }; + + return traverseEntity(visitor, traverseOptions, entry); +}; + +const traverseRelationTarget = (schema) => async (visitor, path, entry) => { + const traverseOptions = { schema, path }; + + return traverseEntity(visitor, traverseOptions, entry); +}; + +const traverseMediaTarget = async (visitor, path, entry) => { + const targetSchemaUID = 'plugin::upload.file'; + const targetSchema = strapi.getModel(targetSchemaUID); + + const traverseOptions = { schema: targetSchema, path }; + + return traverseEntity(visitor, traverseOptions, entry); +}; + +const traverseComponent = async (visitor, path, schema, entry) => { + const traverseOptions = { schema, path }; + + return traverseEntity(visitor, traverseOptions, entry); +}; + +const visitDynamicZoneEntry = async (visitor, path, entry) => { + const targetSchema = strapi.getModel(entry.__component); + const traverseOptions = { schema: targetSchema, path }; + + return traverseEntity(visitor, traverseOptions, entry); +}; const traverseEntity = async (visitor, options, entity) => { const { path = { raw: null, attribute: null }, schema } = options; @@ -11,9 +47,13 @@ const traverseEntity = async (visitor, options, entity) => { } // Don't mutate the original entity object - const copy = cloneDeep(entity); + // only clone at 1st level as the next level will get clone when traversed + const copy = clone(entity); + const visitorUtils = createVisitorUtils({ data: copy }); - for (const key of Object.keys(copy)) { + const keys = Object.keys(copy); + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; // Retrieve the attribute definition associated to the key from the schema const attribute = schema.attributes[key]; @@ -32,7 +72,6 @@ const traverseEntity = async (visitor, options, entity) => { // Visit the current attribute const visitorOptions = { data: copy, schema, key, value: copy[key], attribute, path: newPath }; - const visitorUtils = createVisitorUtils({ data: copy }); await visitor(visitorOptions, visitorUtils); @@ -52,58 +91,62 @@ const traverseEntity = async (visitor, options, entity) => { if (isRelation) { const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph'); - const traverseTarget = (entry) => { - // Handle polymorphic relationships - const targetSchemaUID = isMorphRelation ? entry.__type : attribute.target; - const targetSchema = strapi.getModel(targetSchemaUID); + const method = isMorphRelation + ? traverseMorphRelationTarget + : traverseRelationTarget(strapi.getModel(attribute.target)); - const traverseOptions = { schema: targetSchema, path: newPath }; + if (isArray(value)) { + const res = new Array(value.length); + for (let i = 0; i < value.length; i += 1) { + res[i] = await method(visitor, newPath, value[i]); + } + copy[key] = res; + } else { + copy[key] = await method(visitor, newPath, value); + } - return traverseEntity(visitor, traverseOptions, entry); - }; - - // need to update copy - copy[key] = isArray(value) - ? await Promise.all(value.map(traverseTarget)) - : await traverseTarget(value); + continue; } if (isMedia) { - const traverseTarget = (entry) => { - const targetSchemaUID = 'plugin::upload.file'; - const targetSchema = strapi.getModel(targetSchemaUID); - - const traverseOptions = { schema: targetSchema, path: newPath }; - - return traverseEntity(visitor, traverseOptions, entry); - }; - // need to update copy - copy[key] = isArray(value) - ? await Promise.all(value.map(traverseTarget)) - : await traverseTarget(value); + if (isArray(value)) { + const res = new Array(value.length); + for (let i = 0; i < value.length; i += 1) { + res[i] = await traverseMediaTarget(visitor, newPath, value[i]); + } + copy[key] = res; + } else { + copy[key] = await traverseMediaTarget(visitor, newPath, value); + } + + continue; } if (isComponent) { const targetSchema = strapi.getModel(attribute.component); - const traverseOptions = { schema: targetSchema, path: newPath }; - const traverseComponent = (entry) => traverseEntity(visitor, traverseOptions, entry); + if (isArray(value)) { + const res = new Array(value.length); + for (let i = 0; i < value.length; i += 1) { + res[i] = await traverseComponent(visitor, newPath, targetSchema, value[i]); + } + copy[key] = res; + } else { + copy[key] = await traverseComponent(visitor, newPath, targetSchema, value); + } - copy[key] = isArray(value) - ? await Promise.all(value.map(traverseComponent)) - : await traverseComponent(value); + continue; } if (isDynamicZone && isArray(value)) { - const visitDynamicZoneEntry = (entry) => { - const targetSchema = strapi.getModel(entry.__component); - const traverseOptions = { schema: targetSchema, path: newPath }; + const res = new Array(value.length); + for (let i = 0; i < value.length; i += 1) { + res[i] = await visitDynamicZoneEntry(visitor, newPath, value[i]); + } + copy[key] = res; - return traverseEntity(visitor, traverseOptions, entry); - }; - - copy[key] = await Promise.all(value.map(visitDynamicZoneEntry)); + continue; } }