diff --git a/docs/docs/core/hooks/use-fetch-client.mdx b/docs/docs/core/hooks/use-fetch-client.mdx
new file mode 100644
index 0000000000..83d28fe201
--- /dev/null
+++ b/docs/docs/core/hooks/use-fetch-client.mdx
@@ -0,0 +1,103 @@
+---
+title: useFetchClient
+slug: /hooks/use-fetch-client
+description: API reference for the useFetchClient hook in Strapi
+tags:
+ - hooks
+ - axios
+ - data
+---
+
+## Usage
+
+The following example shows a basic way to use the `useFetchClient` hook to make a get request to a Strapi backend endpoint:
+
+```jsx
+import {useState} from "react"
+import useFetchClient from '@strapi/admin/admin/src/hooks/useFetchClient';
+
+const Component = () => {
+ const [items, setItems] = useState([]);
+ const { get } = useFetchClient();
+ const requestURL = "/some-endpoint";
+
+ const handleGetData = async () => {
+ const { data } = await get(requestURL);
+ setItems(data.items);
+ }
+
+ return(
+
+
+ {
+ items && items.map(item =>
{item.name}
))
+ }
+
+
+
+ )
+}
+```
+
+## Methods
+
+Essentially, this is an abstraction around the axios instance exposed by a hook. It provides a simple interface to handle API calls to the Strapi backend.
+It handles request cancellations inside the hook with an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). This is typically triggered when the component is unmounted so all the requests that it is currently making are aborted.
+
+The hook exposes four methods:
+
+- **get(url, config)**: requires a relative url (`string`) and makes a `GET` request to the Strapi backend with an optional configuration `object` passed as second parameter.
+- **post(url, data, config)**: requires a relative url (`string`) and makes a `POST` request with the required data `object` to the Strapi backend with the optional configuration `object` passed as third parameter.
+- **put(url, data, config)**: requires a relative url (`string`) and makes a `PUT` request with the required data `object` to the Strapi backend with the optional configuration `object` passed as third parameter.
+- **del(url, config)**: requires a relative url (`string`) and makes a `DELETE` request to the Strapi backend with an optional configuration `object` passed as second parameter.
+
+## Implementation details
+
+The following information is the internal additions we've added to the axios instance via two request interceptors. As well as an explanation of the `baseUrl`.
+
+### Base URL
+
+The default URL will be the one defined in the environment variable: `STRAPI_ADMIN_BACKEND_URL`.
+
+### Interceptors
+
+#### Request
+
+The request interceptor adds the following parameters to the header:
+
+```js
+{
+ Authorization: `Bearer `,
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+}
+```
+
+#### Response
+
+If everything works correctly, the response is returned as it comes from the backend. However, if it contains a **status code of 401** the authentication details will be removed from
+the application storage and the window will be reloaded.
+
+:::caution
+Have this in mind if using this hook in pages where the auth token is not available, such as `login`, because it will create an infinite loop. See the [Troubleshooting](#troubleshooting) section for more information.
+:::
+
+## Further Reading
+
+- [axios instance API](https://axios-http.com/docs/instance)
+- [AbortController](https://axios-http.com/docs/cancellation)
+
+## Troubleshooting
+
+### Authentication problems
+
+Trying to access a protected route from a context where the auth token is not available may lead to an infinite loop due to the response interceptor that
+reloads the page when obtaining a 401 response. One option to avoid this from happening is to not consider a 401 response as an error, see below:
+
+```js
+const {
+ data: { data: properties },
+} = await get(`/protected-endpoint`, {
+ validateStatus: (status) => status < 500,
+});
+```
diff --git a/docs/sidebars.js b/docs/sidebars.js
index c64e6f077a..6154e8ff2a 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -39,6 +39,17 @@ const sidebars = {
},
],
},
+ {
+ type: 'category',
+ label: 'Hooks',
+ items: [
+ {
+ type: 'doc',
+ label: 'useFetchClient',
+ id: 'core/hooks/use-fetch-client',
+ },
+ ],
+ },
{
type: 'category',
label: 'Content Type Builder',
diff --git a/packages/core/admin/admin/src/content-manager/pages/App/LeftMenu/index.js b/packages/core/admin/admin/src/content-manager/pages/App/LeftMenu/index.js
index b7e0ddcd2c..cdd05abe1b 100644
--- a/packages/core/admin/admin/src/content-manager/pages/App/LeftMenu/index.js
+++ b/packages/core/admin/admin/src/content-manager/pages/App/LeftMenu/index.js
@@ -22,7 +22,9 @@ import getTrad from '../../../utils/getTrad';
import { makeSelectModelLinks } from '../selectors';
const matchByTitle = (links, search) =>
- matchSorter(links, toLower(search), { keys: [(item) => toLower(item.title)] });
+ search
+ ? matchSorter(links, toLower(search), { keys: [(item) => toLower(item.title)] })
+ : sortBy(links, (object) => object.title.toLowerCase());
const LeftMenu = () => {
const [search, setSearch] = useState('');
@@ -52,9 +54,7 @@ const LeftMenu = () => {
defaultMessage: 'Collection Types',
},
searchable: true,
- links: sortBy(matchByTitle(intlCollectionTypeLinks, search), (object) =>
- object.title.toLowerCase()
- ),
+ links: matchByTitle(intlCollectionTypeLinks, search),
},
{
id: 'singleTypes',
@@ -63,9 +63,7 @@ const LeftMenu = () => {
defaultMessage: 'Single Types',
},
searchable: true,
- links: sortBy(matchByTitle(intlSingleTypeLinks, search), (object) =>
- object.title.toLowerCase()
- ),
+ links: matchByTitle(intlSingleTypeLinks, search),
},
];
diff --git a/packages/core/content-type-builder/admin/src/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.js b/packages/core/content-type-builder/admin/src/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.js
index 16844b831e..a94b94a8a7 100644
--- a/packages/core/content-type-builder/admin/src/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.js
+++ b/packages/core/content-type-builder/admin/src/components/ContentTypeBuilderNav/useContentTypeBuilderMenu.js
@@ -162,7 +162,7 @@ const useContentTypeBuilderMenu = () => {
];
const matchByTitle = (links) =>
- matchSorter(links, toLower(search), { keys: [(item) => toLower(item.title)] });
+ search ? matchSorter(links, toLower(search), { keys: [(item) => toLower(item.title)] }) : links;
const getMenu = () => {
// Maybe we can do it simpler with matchsorter wildcards ?
diff --git a/packages/core/content-type-builder/admin/src/components/FormModal/index.js b/packages/core/content-type-builder/admin/src/components/FormModal/index.js
index 7d9eaa4cca..86e67dfecb 100644
--- a/packages/core/content-type-builder/admin/src/components/FormModal/index.js
+++ b/packages/core/content-type-builder/admin/src/components/FormModal/index.js
@@ -20,6 +20,7 @@ import { ModalLayout, ModalBody, ModalFooter } from '@strapi/design-system/Modal
import { Tabs, Tab, TabGroup, TabPanels, TabPanel } from '@strapi/design-system/Tabs';
import { Flex } from '@strapi/design-system/Flex';
import { Stack } from '@strapi/design-system/Stack';
+import { isEqual } from 'lodash';
import pluginId from '../../pluginId';
import useDataManager from '../../hooks/useDataManager';
import useFormModalNavigation from '../../hooks/useFormModalNavigation';
@@ -790,13 +791,35 @@ const FormModal = () => {
}
};
+ const handleConfirmClose = () => {
+ // eslint-disable-next-line no-alert
+ const confirm = window.confirm(
+ formatMessage({
+ id: 'window.confirm.close-modal.file',
+ defaultMessage: 'Are you sure? Your changes will be lost.',
+ })
+ );
+
+ if (confirm) {
+ onCloseModal();
+
+ dispatch({
+ type: RESET_PROPS,
+ });
+ }
+ };
+
const handleClosed = () => {
// Close the modal
- onCloseModal();
- // Reset the reducer
- dispatch({
- type: RESET_PROPS,
- });
+ if (!isEqual(modifiedData, initialData)) {
+ handleConfirmClose();
+ } else {
+ onCloseModal();
+ // Reset the reducer
+ dispatch({
+ type: RESET_PROPS,
+ });
+ }
};
const sendAdvancedTabEvent = (tab) => {
diff --git a/packages/core/email/server/services/email.js b/packages/core/email/server/services/email.js
index 540585d69e..ed3e93e57b 100644
--- a/packages/core/email/server/services/email.js
+++ b/packages/core/email/server/services/email.js
@@ -1,6 +1,10 @@
'use strict';
const _ = require('lodash');
+const {
+ template: { createStrictInterpolationRegExp },
+ keysDeep,
+} = require('@strapi/utils/');
const getProviderSettings = () => {
return strapi.config.get('plugin.email');
@@ -26,10 +30,19 @@ const sendTemplatedEmail = (emailOptions = {}, emailTemplate = {}, data = {}) =>
);
}
+ const allowedInterpolationVariables = keysDeep(data);
+ const interpolate = createStrictInterpolationRegExp(allowedInterpolationVariables, 'g');
+
const templatedAttributes = attributes.reduce(
(compiled, attribute) =>
emailTemplate[attribute]
- ? Object.assign(compiled, { [attribute]: _.template(emailTemplate[attribute])(data) })
+ ? Object.assign(compiled, {
+ [attribute]: _.template(emailTemplate[attribute], {
+ interpolate,
+ evaluate: false,
+ escape: false,
+ })(data),
+ })
: compiled,
{}
);
diff --git a/packages/core/utils/lib/index.js b/packages/core/utils/lib/index.js
index 7e3aa0f280..3d383e9c60 100644
--- a/packages/core/utils/lib/index.js
+++ b/packages/core/utils/lib/index.js
@@ -24,7 +24,7 @@ const {
joinBy,
toKebabCase,
} = require('./string-formatting');
-const { removeUndefined } = require('./object-formatting');
+const { removeUndefined, keysDeep } = require('./object-formatting');
const { getConfigUrls, getAbsoluteAdminUrl, getAbsoluteServerUrl } = require('./config');
const { generateTimestampCode } = require('./code-generator');
const contentTypes = require('./content-types');
@@ -40,6 +40,7 @@ const traverseEntity = require('./traverse-entity');
const pipeAsync = require('./pipe-async');
const convertQueryParams = require('./convert-query-params');
const importDefault = require('./import-default');
+const template = require('./template');
module.exports = {
yup,
@@ -61,11 +62,13 @@ module.exports = {
getConfigUrls,
escapeQuery,
removeUndefined,
+ keysDeep,
getAbsoluteAdminUrl,
getAbsoluteServerUrl,
generateTimestampCode,
stringIncludes,
stringEquals,
+ template,
isKebabCase,
isCamelCase,
toKebabCase,
diff --git a/packages/core/utils/lib/object-formatting.js b/packages/core/utils/lib/object-formatting.js
index 23c1326db8..24c1b74718 100644
--- a/packages/core/utils/lib/object-formatting.js
+++ b/packages/core/utils/lib/object-formatting.js
@@ -4,6 +4,12 @@ const _ = require('lodash');
const removeUndefined = (obj) => _.pickBy(obj, (value) => typeof value !== 'undefined');
+const keysDeep = (obj, path = []) =>
+ !_.isObject(obj)
+ ? path.join('.')
+ : _.reduce(obj, (acc, next, key) => _.concat(acc, keysDeep(next, [...path, key])), []);
+
module.exports = {
removeUndefined,
+ keysDeep,
};
diff --git a/packages/core/utils/lib/template.js b/packages/core/utils/lib/template.js
new file mode 100644
index 0000000000..35579cb566
--- /dev/null
+++ b/packages/core/utils/lib/template.js
@@ -0,0 +1,28 @@
+'use strict';
+
+/**
+ * Create a strict interpolation RegExp based on the given variables' name
+ *
+ * @param {string[]} allowedVariableNames - The list of allowed variables
+ * @param {string} [flags] - The RegExp flags
+ */
+const createStrictInterpolationRegExp = (allowedVariableNames, flags) => {
+ const oneOfVariables = allowedVariableNames.join('|');
+
+ // 1. We need to match the delimiters: <%= ... %>
+ // 2. We accept any number of whitespaces characters before and/or after the variable name: \s* ... \s*
+ // 3. We only accept values from the variable list as interpolation variables' name: : (${oneOfVariables})
+ return new RegExp(`<%=\\s*(${oneOfVariables})\\s*%>`, flags);
+};
+
+/**
+ * Create a loose interpolation RegExp to match as many groups as possible
+ *
+ * @param {string} [flags] - The RegExp flags
+ */
+const createLooseInterpolationRegExp = (flags) => new RegExp(/<%=([\s\S]+?)%>/, flags);
+
+module.exports = {
+ createStrictInterpolationRegExp,
+ createLooseInterpolationRegExp,
+};
diff --git a/packages/plugins/users-permissions/server/controllers/validation/__tests__/email-template.test.js b/packages/plugins/users-permissions/server/controllers/validation/__tests__/email-template.test.js
index f4e6b054f1..27ba81aca7 100644
--- a/packages/plugins/users-permissions/server/controllers/validation/__tests__/email-template.test.js
+++ b/packages/plugins/users-permissions/server/controllers/validation/__tests__/email-template.test.js
@@ -17,6 +17,11 @@ describe('isValidEmailTemplate', () => {
expect(isValidEmailTemplate('<%CODE%>')).toBe(false);
expect(isValidEmailTemplate('${CODE}')).toBe(false);
expect(isValidEmailTemplate('${ CODE }')).toBe(false);
+ expect(
+ isValidEmailTemplate(
+ '<%=`${ console.log({ "remote-execution": { "foo": "bar" }/*<>%=*/ }) }`%>'
+ )
+ ).toBe(false);
});
test('Fails on non authorized keys', () => {
diff --git a/packages/plugins/users-permissions/server/controllers/validation/email-template.js b/packages/plugins/users-permissions/server/controllers/validation/email-template.js
index 071ad11a81..49b3f765f7 100644
--- a/packages/plugins/users-permissions/server/controllers/validation/email-template.js
+++ b/packages/plugins/users-permissions/server/controllers/validation/email-template.js
@@ -1,8 +1,17 @@
'use strict';
-const _ = require('lodash');
+const { trim } = require('lodash/fp');
+const {
+ template: { createLooseInterpolationRegExp, createStrictInterpolationRegExp },
+} = require('@strapi/utils');
+
+const invalidPatternsRegexes = [
+ // Ignore "evaluation" patterns: <% ... %>
+ /<%[^=]([\s\S]*?)%>/m,
+ // Ignore basic string interpolations
+ /\${([^{}]*)}/m,
+];
-const invalidPatternsRegexes = [/<%[^=]([^<>%]*)%>/m, /\${([^{}]*)}/m];
const authorizedKeys = [
'URL',
'ADMIN_URL',
@@ -19,27 +28,42 @@ const matchAll = (pattern, src) => {
let match;
const regexPatternWithGlobal = RegExp(pattern, 'g');
+
// eslint-disable-next-line no-cond-assign
while ((match = regexPatternWithGlobal.exec(src))) {
const [, group] = match;
- matches.push(_.trim(group));
+ matches.push(trim(group));
}
+
return matches;
};
const isValidEmailTemplate = (template) => {
+ // Check for known invalid patterns
for (const reg of invalidPatternsRegexes) {
if (reg.test(template)) {
return false;
}
}
- const matches = matchAll(/<%=([^<>%=]*)%>/, template);
- for (const match of matches) {
- if (!authorizedKeys.includes(match)) {
- return false;
- }
+ const interpolation = {
+ // Strict interpolation pattern to match only valid groups
+ strict: createStrictInterpolationRegExp(authorizedKeys),
+ // Weak interpolation pattern to match as many group as possible.
+ loose: createLooseInterpolationRegExp(),
+ };
+
+ // Compute both strict & loose matches
+ const strictMatches = matchAll(interpolation.strict, template);
+ const looseMatches = matchAll(interpolation.loose, template);
+
+ // If we have more matches with the loose RegExp than with the strict one,
+ // then it means that at least one of the interpolation group is invalid
+ // Note: In the future, if we wanted to give more details for error formatting
+ // purposes, we could return the difference between the two arrays
+ if (looseMatches.length > strictMatches.length) {
+ return false;
}
return true;
diff --git a/packages/plugins/users-permissions/server/services/user.js b/packages/plugins/users-permissions/server/services/user.js
index c7a37abb79..27cf71abdd 100644
--- a/packages/plugins/users-permissions/server/services/user.js
+++ b/packages/plugins/users-permissions/server/services/user.js
@@ -109,17 +109,25 @@ module.exports = ({ strapi }) => ({
await this.edit(user.id, { confirmationToken });
const apiPrefix = strapi.config.get('api.rest.prefix');
- settings.message = await userPermissionService.template(settings.message, {
- URL: urlJoin(getAbsoluteServerUrl(strapi.config), apiPrefix, '/auth/email-confirmation'),
- SERVER_URL: getAbsoluteServerUrl(strapi.config),
- ADMIN_URL: getAbsoluteAdminUrl(strapi.config),
- USER: sanitizedUserInfo,
- CODE: confirmationToken,
- });
- settings.object = await userPermissionService.template(settings.object, {
- USER: sanitizedUserInfo,
- });
+ try {
+ settings.message = await userPermissionService.template(settings.message, {
+ URL: urlJoin(getAbsoluteServerUrl(strapi.config), apiPrefix, '/auth/email-confirmation'),
+ SERVER_URL: getAbsoluteServerUrl(strapi.config),
+ ADMIN_URL: getAbsoluteAdminUrl(strapi.config),
+ USER: sanitizedUserInfo,
+ CODE: confirmationToken,
+ });
+
+ settings.object = await userPermissionService.template(settings.object, {
+ USER: sanitizedUserInfo,
+ });
+ } catch {
+ strapi.log.error(
+ '[plugin::users-permissions.sendConfirmationEmail]: Failed to generate a template for "user confirmation email". Please make sure your email template is valid and does not contain invalid characters or patterns'
+ );
+ return;
+ }
// Send an email to the user.
await strapi
diff --git a/packages/plugins/users-permissions/server/services/users-permissions.js b/packages/plugins/users-permissions/server/services/users-permissions.js
index 1389cd582e..3389cdb532 100644
--- a/packages/plugins/users-permissions/server/services/users-permissions.js
+++ b/packages/plugins/users-permissions/server/services/users-permissions.js
@@ -3,6 +3,11 @@
const _ = require('lodash');
const { filter, map, pipe, prop } = require('lodash/fp');
const urlJoin = require('url-join');
+const {
+ template: { createStrictInterpolationRegExp },
+ errors,
+ keysDeep,
+} = require('@strapi/utils');
const { getService } = require('../utils');
@@ -230,7 +235,15 @@ module.exports = ({ strapi }) => ({
},
template(layout, data) {
- const compiledObject = _.template(layout);
- return compiledObject(data);
+ const allowedTemplateVariables = keysDeep(data);
+
+ // Create a strict interpolation RegExp based on possible variable names
+ const interpolate = createStrictInterpolationRegExp(allowedTemplateVariables, 'g');
+
+ try {
+ return _.template(layout, { interpolate, evaluate: false, escape: false })(data);
+ } catch (e) {
+ throw new errors.ApplicationError('Invalid email template');
+ }
},
});
diff --git a/packages/providers/upload-aws-s3/package.json b/packages/providers/upload-aws-s3/package.json
index 7972e6921c..e5a12596bc 100644
--- a/packages/providers/upload-aws-s3/package.json
+++ b/packages/providers/upload-aws-s3/package.json
@@ -37,7 +37,7 @@
"test": "echo \"no tests yet\""
},
"dependencies": {
- "aws-sdk": "2.1260.0",
+ "aws-sdk": "2.1287.0",
"lodash": "4.17.21"
},
"engines": {
diff --git a/yarn.lock b/yarn.lock
index 86c024e3a8..c61209b49b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7792,10 +7792,10 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
-aws-sdk@2.1260.0:
- version "2.1260.0"
- resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1260.0.tgz#3dc18f49dd4aaa18e4e9787c62f1ad02e5aaac4c"
- integrity sha512-iciXVukPbhmh44xcF+5/CO15jtESqRkXuEH54XaU8IpCzbYkAcPBaS29vLRN2SRuN1Dy2S3X7SaZZxFJWLAHrg==
+aws-sdk@2.1287.0:
+ version "2.1287.0"
+ resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1287.0.tgz#40296aadf34e7550e058f73f3363130d65ffb1c0"
+ integrity sha512-mtfDstUdFNn8FnBaXs2KAaQ0cgDIiwlqwC2UptUKWWrugjZHAoRacfD/6bnah1Kwhu43F9CDEe5QLHnQtymNkw==
dependencies:
buffer "4.9.2"
events "1.1.1"
@@ -15014,9 +15014,9 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
json5@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
- integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
+ integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
dependencies:
minimist "^1.2.0"
@@ -16463,12 +16463,7 @@ minimist-options@4.1.0:
is-plain-obj "^1.1.0"
kind-of "^6.0.3"
-minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
- version "1.2.6"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
- integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
-
-minimist@^1.2.0:
+minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
@@ -22367,19 +22362,7 @@ util@^0.11.0:
dependencies:
inherits "2.0.3"
-util@^0.12.0, util@^0.12.4:
- version "0.12.4"
- resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253"
- integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==
- dependencies:
- inherits "^2.0.3"
- is-arguments "^1.0.4"
- is-generator-function "^1.0.7"
- is-typed-array "^1.1.3"
- safe-buffer "^5.1.2"
- which-typed-array "^1.1.2"
-
-util@^0.12.3:
+util@^0.12.0, util@^0.12.3, util@^0.12.4:
version "0.12.5"
resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==