1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

fix(settings): fixed two IPFS gateway issues (#19700)

* fix(settings): fixed two IPFS gateway issues

- adds back in two bugfixes that were originally in #19283
  - fixes #16871
  - fixes #18140

- achieves 100% code coverage for /ui/pages/settings/security-tab
- removes the npm package `valid-url`, which has not been updated in 10 years

* changes after #20172 was merged

* improved URL validation (specifically spaces)

* better Jest coverage

* response to legobeat review

* fixing lint and Jest
This commit is contained in:
Howard Braham 2023-08-22 22:13:13 -07:00 committed by GitHub
parent b8525566f2
commit d3d30fd373
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 381 additions and 227 deletions

View File

@ -1,16 +1,16 @@
import { ethErrors, errorCodes } from 'eth-rpc-errors';
import validUrl from 'valid-url';
import { omit } from 'lodash';
import { ApprovalType } from '@metamask/controller-utils'; import { ApprovalType } from '@metamask/controller-utils';
import { errorCodes, ethErrors } from 'eth-rpc-errors';
import { omit } from 'lodash';
import { import {
MESSAGE_TYPE, MESSAGE_TYPE,
UNKNOWN_TICKER_SYMBOL, UNKNOWN_TICKER_SYMBOL,
} from '../../../../../shared/constants/app'; } from '../../../../../shared/constants/app';
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics';
import { import {
isPrefixedFormattedHexString, isPrefixedFormattedHexString,
isSafeChainId, isSafeChainId,
} from '../../../../../shared/modules/network.utils'; } from '../../../../../shared/modules/network.utils';
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics'; import { getValidUrl } from '../../util';
const addEthereumChain = { const addEthereumChain = {
methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN], methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN],
@ -83,27 +83,25 @@ async function addEthereumChainHandler(
); );
} }
const isLocalhost = (strUrl) => { function isLocalhostOrHttps(urlString) {
try { const url = getValidUrl(urlString);
const url = new URL(strUrl);
return url.hostname === 'localhost' || url.hostname === '127.0.0.1'; return (
} catch (error) { url !== null &&
return false; (url.hostname === 'localhost' ||
} url.hostname === '127.0.0.1' ||
}; url.protocol === 'https:')
);
}
const firstValidRPCUrl = Array.isArray(rpcUrls) const firstValidRPCUrl = Array.isArray(rpcUrls)
? rpcUrls.find( ? rpcUrls.find((rpcUrl) => isLocalhostOrHttps(rpcUrl))
(rpcUrl) => isLocalhost(rpcUrl) || validUrl.isHttpsUri(rpcUrl),
)
: null; : null;
const firstValidBlockExplorerUrl = const firstValidBlockExplorerUrl =
blockExplorerUrls !== null && Array.isArray(blockExplorerUrls) blockExplorerUrls !== null && Array.isArray(blockExplorerUrls)
? blockExplorerUrls.find( ? blockExplorerUrls.find((blockExplorerUrl) =>
(blockExplorerUrl) => isLocalhostOrHttps(blockExplorerUrl),
isLocalhost(blockExplorerUrl) ||
validUrl.isHttpsUri(blockExplorerUrl),
) )
: null; : null;

View File

@ -1,24 +1,27 @@
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import { import {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN,
ENVIRONMENT_TYPE_BACKGROUND, ENVIRONMENT_TYPE_BACKGROUND,
PLATFORM_FIREFOX, ENVIRONMENT_TYPE_FULLSCREEN,
PLATFORM_OPERA, ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_POPUP,
PLATFORM_CHROME, PLATFORM_CHROME,
PLATFORM_EDGE, PLATFORM_EDGE,
PLATFORM_FIREFOX,
PLATFORM_OPERA,
} from '../../../shared/constants/app'; } from '../../../shared/constants/app';
import { import {
TransactionEnvelopeType,
TransactionStatus, TransactionStatus,
TransactionType, TransactionType,
TransactionEnvelopeType,
} from '../../../shared/constants/transaction'; } from '../../../shared/constants/transaction';
import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils';
import { import {
addUrlProtocolPrefix,
deferredPromise, deferredPromise,
formatTxMetaForRpcResult,
getEnvironmentType, getEnvironmentType,
getPlatform, getPlatform,
formatTxMetaForRpcResult, getValidUrl,
isWebUrl,
} from './util'; } from './util';
describe('app utils', () => { describe('app utils', () => {
@ -73,6 +76,39 @@ describe('app utils', () => {
}); });
}); });
describe('URL utils', () => {
it('should test addUrlProtocolPrefix', () => {
expect(addUrlProtocolPrefix('http://example.com')).toStrictEqual(
'http://example.com',
);
expect(addUrlProtocolPrefix('https://example.com')).toStrictEqual(
'https://example.com',
);
expect(addUrlProtocolPrefix('example.com')).toStrictEqual(
'https://example.com',
);
expect(addUrlProtocolPrefix('exa mple.com')).toStrictEqual(null);
});
it('should test isWebUrl', () => {
expect(isWebUrl('http://example.com')).toStrictEqual(true);
expect(isWebUrl('https://example.com')).toStrictEqual(true);
expect(isWebUrl('https://exa mple.com')).toStrictEqual(false);
expect(isWebUrl('')).toStrictEqual(false);
});
it('should test getValidUrl', () => {
expect(getValidUrl('http://example.com').toString()).toStrictEqual(
'http://example.com/',
);
expect(getValidUrl('https://example.com').toString()).toStrictEqual(
'https://example.com/',
);
expect(getValidUrl('https://exa%20mple.com')).toStrictEqual(null);
expect(getValidUrl('')).toStrictEqual(null);
});
});
describe('isPrefixedFormattedHexString', () => { describe('isPrefixedFormattedHexString', () => {
it('should return true for valid hex strings', () => { it('should return true for valid hex strings', () => {
expect(isPrefixedFormattedHexString('0x1')).toStrictEqual(true); expect(isPrefixedFormattedHexString('0x1')).toStrictEqual(true);

View File

@ -1,24 +1,24 @@
import urlLib from 'url';
import { AccessList } from '@ethereumjs/tx';
import BN from 'bn.js'; import BN from 'bn.js';
import { memoize } from 'lodash'; import { memoize } from 'lodash';
import { AccessList } from '@ethereumjs/tx';
import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
import { import {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN,
ENVIRONMENT_TYPE_BACKGROUND, ENVIRONMENT_TYPE_BACKGROUND,
PLATFORM_FIREFOX, ENVIRONMENT_TYPE_FULLSCREEN,
PLATFORM_OPERA, ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_POPUP,
PLATFORM_BRAVE,
PLATFORM_CHROME, PLATFORM_CHROME,
PLATFORM_EDGE, PLATFORM_EDGE,
PLATFORM_BRAVE, PLATFORM_FIREFOX,
PLATFORM_OPERA,
} from '../../../shared/constants/app'; } from '../../../shared/constants/app';
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
import { import {
TransactionEnvelopeType, TransactionEnvelopeType,
TransactionMeta, TransactionMeta,
} from '../../../shared/constants/transaction'; } from '../../../shared/constants/transaction';
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
/** /**
* @see {@link getEnvironmentType} * @see {@link getEnvironmentType}
@ -143,13 +143,13 @@ function checkAlarmExists(alarmList: { name: string }[], alarmName: string) {
} }
export { export {
getPlatform,
getEnvironmentType,
hexToBn,
BnMultiplyByFraction, BnMultiplyByFraction,
addHexPrefix, addHexPrefix,
getChainType,
checkAlarmExists, checkAlarmExists,
getChainType,
getEnvironmentType,
getPlatform,
hexToBn,
}; };
// Taken from https://stackoverflow.com/a/1349426/3696652 // Taken from https://stackoverflow.com/a/1349426/3696652
@ -235,10 +235,43 @@ export function previousValueComparator<A>(
} }
export function addUrlProtocolPrefix(urlString: string) { export function addUrlProtocolPrefix(urlString: string) {
if (!urlString.match(/(^http:\/\/)|(^https:\/\/)/u)) { let trimmed = urlString.trim();
return `https://${urlString}`;
if (trimmed.length && !urlLib.parse(trimmed).protocol) {
trimmed = `https://${trimmed}`;
} }
return urlString;
if (getValidUrl(trimmed) !== null) {
return trimmed;
}
return null;
}
export function getValidUrl(urlString: string): URL | null {
try {
const url = new URL(urlString);
if (url.hostname.length === 0 || url.pathname.length === 0) {
return null;
}
if (url.hostname !== decodeURIComponent(url.hostname)) {
return null; // will happen if there's a %, a space, or other invalid character in the hostname
}
return url;
} catch (error) {
return null;
}
}
export function isWebUrl(urlString: string): boolean {
const url = getValidUrl(urlString);
return (
url !== null && (url.protocol === 'https:' || url.protocol === 'http:')
);
} }
interface FormattedTransactionMeta { interface FormattedTransactionMeta {

View File

@ -8,7 +8,7 @@ module.exports = {
'<rootDir>/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.ts', '<rootDir>/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.ts',
'<rootDir>/app/scripts/controllers/transactions/IncomingTransactionHelper.ts', '<rootDir>/app/scripts/controllers/transactions/IncomingTransactionHelper.ts',
'<rootDir>/app/scripts/flask/**/*.js', '<rootDir>/app/scripts/flask/**/*.js',
'<rootDir>/app/scripts/lib/**/*.js', '<rootDir>/app/scripts/lib/**/*.(js|ts)',
'<rootDir>/app/scripts/lib/createRPCMethodTrackingMiddleware.js', '<rootDir>/app/scripts/lib/createRPCMethodTrackingMiddleware.js',
'<rootDir>/app/scripts/migrations/*.js', '<rootDir>/app/scripts/migrations/*.js',
'<rootDir>/app/scripts/migrations/*.ts', '<rootDir>/app/scripts/migrations/*.ts',
@ -48,8 +48,7 @@ module.exports = {
'<rootDir>/app/scripts/controllers/sign.test.ts', '<rootDir>/app/scripts/controllers/sign.test.ts',
'<rootDir>/app/scripts/controllers/decrypt-message.test.ts', '<rootDir>/app/scripts/controllers/decrypt-message.test.ts',
'<rootDir>/app/scripts/flask/**/*.test.js', '<rootDir>/app/scripts/flask/**/*.test.js',
'<rootDir>/app/scripts/lib/**/*.test.js', '<rootDir>/app/scripts/lib/**/*.test.(js|ts)',
'<rootDir>/app/scripts/lib/**/*.test.ts',
'<rootDir>/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js', '<rootDir>/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js',
'<rootDir>/app/scripts/migrations/*.test.(js|ts)', '<rootDir>/app/scripts/migrations/*.test.(js|ts)',
'<rootDir>/app/scripts/platforms/*.test.js', '<rootDir>/app/scripts/platforms/*.test.js',

View File

@ -362,7 +362,6 @@
"single-call-balance-checker-abi": "^1.0.0", "single-call-balance-checker-abi": "^1.0.0",
"unicode-confusables": "^0.1.1", "unicode-confusables": "^0.1.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"valid-url": "^1.0.9",
"web3-stream-provider": "^4.0.0", "web3-stream-provider": "^4.0.0",
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },

View File

@ -340,6 +340,8 @@
"unapprovedTypedMessagesCount": 0, "unapprovedTypedMessagesCount": 0,
"useTokenDetection": true, "useTokenDetection": true,
"useCurrencyRateCheck": true, "useCurrencyRateCheck": true,
"useNftDetection": true,
"openSeaEnabled": true,
"advancedGasFee": { "advancedGasFee": {
"maxBaseFee": "75", "maxBaseFee": "75",
"priorityFee": "2" "priorityFee": "2"

View File

@ -129,7 +129,7 @@ const RevealSeedPage = () => {
const renderPasswordPromptContent = () => { const renderPasswordPromptContent = () => {
return ( return (
<form onSubmit={(event) => handleSubmit(event)}> <form onSubmit={handleSubmit}>
<Label htmlFor="password-box">{t('enterPasswordContinue')}</Label> <Label htmlFor="password-box">{t('enterPasswordContinue')}</Label>
<TextField <TextField
inputProps={{ inputProps={{

View File

@ -1,3 +1,7 @@
import classnames from 'classnames';
import { isEqual } from 'lodash';
import log from 'loglevel';
import PropTypes from 'prop-types';
import React, { import React, {
useCallback, useCallback,
useContext, useContext,
@ -6,12 +10,18 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types'; import { isWebUrl } from '../../../../../app/scripts/lib/util';
import validUrl from 'valid-url'; import {
import log from 'loglevel'; MetaMetricsEventCategory,
import classnames from 'classnames'; MetaMetricsEventName,
import { isEqual } from 'lodash'; MetaMetricsNetworkEventSource,
import { useI18nContext } from '../../../../hooks/useI18nContext'; } from '../../../../../shared/constants/metametrics';
import {
FEATURED_RPCS,
infuraProjectId,
} from '../../../../../shared/constants/network';
import fetchWithCache from '../../../../../shared/lib/fetch-with-cache';
import { decimalToHex } from '../../../../../shared/modules/conversion.utils';
import { import {
isPrefixedFormattedHexString, isPrefixedFormattedHexString,
isSafeChainId, isSafeChainId,
@ -20,27 +30,17 @@ import { jsonRpcRequest } from '../../../../../shared/modules/rpc.utils';
import ActionableMessage from '../../../../components/ui/actionable-message'; import ActionableMessage from '../../../../components/ui/actionable-message';
import Button from '../../../../components/ui/button'; import Button from '../../../../components/ui/button';
import FormField from '../../../../components/ui/form-field'; import FormField from '../../../../components/ui/form-field';
import {
setSelectedNetworkConfigurationId,
upsertNetworkConfiguration,
editAndSetNetworkConfiguration,
showModal,
setNewNetworkAdded,
} from '../../../../store/actions';
import fetchWithCache from '../../../../../shared/lib/fetch-with-cache';
import { usePrevious } from '../../../../hooks/usePrevious';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
MetaMetricsNetworkEventSource,
} from '../../../../../shared/constants/metametrics';
import {
infuraProjectId,
FEATURED_RPCS,
} from '../../../../../shared/constants/network';
import { decimalToHex } from '../../../../../shared/modules/conversion.utils';
import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { MetaMetricsContext } from '../../../../contexts/metametrics';
import { getNetworkLabelKey } from '../../../../helpers/utils/i18n-helper'; import { getNetworkLabelKey } from '../../../../helpers/utils/i18n-helper';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { usePrevious } from '../../../../hooks/usePrevious';
import {
editAndSetNetworkConfiguration,
setNewNetworkAdded,
setSelectedNetworkConfigurationId,
showModal,
upsertNetworkConfiguration,
} from '../../../../store/actions';
/** /**
* Attempts to convert the given chainId to a decimal string, for display * Attempts to convert the given chainId to a decimal string, for display
@ -74,11 +74,6 @@ const prefixChainId = (chainId) => {
return prefixedChainId; return prefixedChainId;
}; };
const isValidWhenAppended = (url) => {
const appendedRpc = `http://${url}`;
return validUrl.isWebUri(appendedRpc) && !url.match(/^https?:\/\/$/u);
};
const NetworksForm = ({ const NetworksForm = ({
addNewNetwork, addNewNetwork,
restrictHeight, restrictHeight,
@ -208,23 +203,20 @@ const NetworksForm = ({
const validateBlockExplorerURL = useCallback( const validateBlockExplorerURL = useCallback(
(url) => { (url) => {
if (!validUrl.isWebUri(url) && url !== '') { if (url.length > 0 && !isWebUrl(url)) {
let errorKey; if (isWebUrl(`https://${url}`)) {
let errorMessage; return {
key: 'urlErrorMsg',
if (isValidWhenAppended(url)) { msg: t('urlErrorMsg'),
errorKey = 'urlErrorMsg'; };
errorMessage = t('urlErrorMsg');
} else {
errorKey = 'invalidBlockExplorerURL';
errorMessage = t('invalidBlockExplorerURL');
} }
return { return {
key: errorKey, key: 'invalidBlockExplorerURL',
msg: errorMessage, msg: t('invalidBlockExplorerURL'),
}; };
} }
return null; return null;
}, },
[t], [t],
@ -407,7 +399,6 @@ const NetworksForm = ({
const validateRPCUrl = useCallback( const validateRPCUrl = useCallback(
(url) => { (url) => {
const isValidUrl = validUrl.isWebUri(url);
const [ const [
{ {
rpcUrl: matchingRPCUrl = null, rpcUrl: matchingRPCUrl = null,
@ -417,20 +408,16 @@ const NetworksForm = ({
] = networksToRender.filter((e) => e.rpcUrl === url); ] = networksToRender.filter((e) => e.rpcUrl === url);
const { rpcUrl: selectedNetworkRpcUrl } = selectedNetwork; const { rpcUrl: selectedNetworkRpcUrl } = selectedNetwork;
if (!isValidUrl && url !== '') { if (url.length > 0 && !isWebUrl(url)) {
let errorKey; if (isWebUrl(`https://${url}`)) {
let errorMessage; return {
if (isValidWhenAppended(url)) { key: 'urlErrorMsg',
errorKey = 'urlErrorMsg'; msg: t('urlErrorMsg'),
errorMessage = t('urlErrorMsg'); };
} else {
errorKey = 'invalidRPC';
errorMessage = t('invalidRPC');
} }
return { return {
key: errorKey, key: 'invalidRPC',
msg: errorMessage, msg: t('invalidRPC'),
}; };
} else if (matchingRPCUrl && matchingRPCUrl !== selectedNetworkRpcUrl) { } else if (matchingRPCUrl && matchingRPCUrl !== selectedNetworkRpcUrl) {
return { return {

View File

@ -448,6 +448,7 @@ exports[`Security Tab should match snapshot 1`] = `
</div> </div>
<div <div
class="settings-page__content-item-col" class="settings-page__content-item-col"
data-testid="ipfsToggle"
> >
<label <label
class="toggle-button toggle-button--on" class="toggle-button toggle-button--on"
@ -476,7 +477,7 @@ exports[`Security Tab should match snapshot 1`] = `
<input <input
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;"
type="checkbox" type="checkbox"
value="dweb.link" value="true"
/> />
</div> </div>
<div <div
@ -786,54 +787,58 @@ exports[`Security Tab should match snapshot 1`] = `
</div> </div>
</div> </div>
<div <div
class="settings-page__content-item-col" class="settings-page__content-item"
data-testid="displayNftMedia"
> >
<label <div
class="toggle-button toggle-button--off" class="settings-page__content-item-col"
tabindex="0" data-testid="enableOpenSeaAPI"
> >
<div <label
style="display: flex; width: 52px; align-items: center; justify-content: flex-start; position: relative; cursor: pointer; background-color: transparent; border: 0px; padding: 0px; user-select: none;" class="toggle-button toggle-button--on"
tabindex="0"
> >
<div <div
style="width: 40px; height: 24px; padding: 0px; border-radius: 26px; display: flex; align-items: center; justify-content: center; background-color: rgb(242, 244, 246);" style="display: flex; width: 52px; align-items: center; justify-content: flex-start; position: relative; cursor: pointer; background-color: transparent; border: 0px; padding: 0px; user-select: none;"
> >
<div <div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgb(250, 250, 250); margin-top: auto; margin-bottom: auto; line-height: 0; opacity: 0; width: 26px; height: 20px; left: 4px;" style="width: 40px; height: 24px; padding: 0px; border-radius: 26px; display: flex; align-items: center; justify-content: center; background-color: rgb(242, 244, 246);"
/> >
<div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgb(250, 250, 250); margin-top: auto; margin-bottom: auto; line-height: 0; opacity: 1; width: 26px; height: 20px; left: 4px;"
/>
<div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgba(255, 255, 255, 0.6); bottom: 0px; margin-top: auto; margin-bottom: auto; padding-right: 5px; line-height: 0; width: 26px; height: 20px; opacity: 0;"
/>
</div>
<div <div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgba(255, 255, 255, 0.6); bottom: 0px; margin-top: auto; margin-bottom: auto; padding-right: 5px; line-height: 0; width: 26px; height: 20px; opacity: 1;" style="position: absolute; height: 100%; top: 0px; left: 0px; display: flex; flex: 1; align-self: stretch; align-items: center; justify-content: flex-start;"
>
<div
style="width: 18px; height: 18px; display: flex; align-self: center; box-shadow: none; border-radius: 50%; box-sizing: border-box; position: relative; background-color: rgb(3, 125, 214); left: 18px;"
/>
</div>
<input
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;"
type="checkbox"
value="true"
/> />
</div> </div>
<div <div
style="position: absolute; height: 100%; top: 0px; left: 0px; display: flex; flex: 1; align-self: stretch; align-items: center; justify-content: flex-start;" class="toggle-button__status"
> >
<div <span
style="width: 18px; height: 18px; display: flex; align-self: center; box-shadow: none; border-radius: 50%; box-sizing: border-box; position: relative; background-color: rgb(106, 115, 125); left: 3px;" class="toggle-button__label-off"
/> >
Off
</span>
<span
class="toggle-button__label-on"
>
On
</span>
</div> </div>
<input </label>
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;" </div>
type="checkbox"
value="false"
/>
</div>
<div
class="toggle-button__status"
>
<span
class="toggle-button__label-off"
>
Off
</span>
<span
class="toggle-button__label-on"
>
On
</span>
</div>
</label>
</div> </div>
</div> </div>
<div <div
@ -878,7 +883,7 @@ exports[`Security Tab should match snapshot 1`] = `
data-testid="useNftDetection" data-testid="useNftDetection"
> >
<label <label
class="toggle-button toggle-button--off" class="toggle-button toggle-button--on"
tabindex="0" tabindex="0"
> >
<div <div
@ -888,23 +893,23 @@ exports[`Security Tab should match snapshot 1`] = `
style="width: 40px; height: 24px; padding: 0px; border-radius: 26px; display: flex; align-items: center; justify-content: center; background-color: rgb(242, 244, 246);" style="width: 40px; height: 24px; padding: 0px; border-radius: 26px; display: flex; align-items: center; justify-content: center; background-color: rgb(242, 244, 246);"
> >
<div <div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgb(250, 250, 250); margin-top: auto; margin-bottom: auto; line-height: 0; opacity: 0; width: 26px; height: 20px; left: 4px;" style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgb(250, 250, 250); margin-top: auto; margin-bottom: auto; line-height: 0; opacity: 1; width: 26px; height: 20px; left: 4px;"
/> />
<div <div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgba(255, 255, 255, 0.6); bottom: 0px; margin-top: auto; margin-bottom: auto; padding-right: 5px; line-height: 0; width: 26px; height: 20px; opacity: 1;" style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgba(255, 255, 255, 0.6); bottom: 0px; margin-top: auto; margin-bottom: auto; padding-right: 5px; line-height: 0; width: 26px; height: 20px; opacity: 0;"
/> />
</div> </div>
<div <div
style="position: absolute; height: 100%; top: 0px; left: 0px; display: flex; flex: 1; align-self: stretch; align-items: center; justify-content: flex-start;" style="position: absolute; height: 100%; top: 0px; left: 0px; display: flex; flex: 1; align-self: stretch; align-items: center; justify-content: flex-start;"
> >
<div <div
style="width: 18px; height: 18px; display: flex; align-self: center; box-shadow: none; border-radius: 50%; box-sizing: border-box; position: relative; background-color: rgb(106, 115, 125); left: 3px;" style="width: 18px; height: 18px; display: flex; align-self: center; box-shadow: none; border-radius: 50%; box-sizing: border-box; position: relative; background-color: rgb(3, 125, 214); left: 18px;"
/> />
</div> </div>
<input <input
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;" style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;"
type="checkbox" type="checkbox"
value="false" value="true"
/> />
</div> </div>
<div <div

View File

@ -23,8 +23,8 @@ import {
import SRPQuiz from '../../../components/app/srp-quiz-modal/SRPQuiz'; import SRPQuiz from '../../../components/app/srp-quiz-modal/SRPQuiz';
import { import {
BUTTON_SIZES, BUTTON_SIZES,
Button,
Box, Box,
Button,
Text, Text,
} from '../../../components/component-library'; } from '../../../components/component-library';
import TextField from '../../../components/ui/text-field'; import TextField from '../../../components/ui/text-field';
@ -76,10 +76,10 @@ export default class SecurityTab extends PureComponent {
}; };
state = { state = {
ipfsGateway: this.props.ipfsGateway, ipfsGateway: this.props.ipfsGateway || IPFS_DEFAULT_GATEWAY_URL,
ipfsGatewayError: '', ipfsGatewayError: '',
srpQuizModalVisible: false, srpQuizModalVisible: false,
ipfsToggle: false, ipfsToggle: this.props.ipfsGateway.length > 0,
}; };
settingsRefCounter = 0; settingsRefCounter = 0;
@ -367,51 +367,40 @@ export default class SecurityTab extends PureComponent {
renderIpfsGatewayControl() { renderIpfsGatewayControl() {
const { t } = this.context; const { t } = this.context;
const { ipfsGatewayError } = this.state; let ipfsError = '';
const { useAddressBarEnsResolution, setUseAddressBarEnsResolution } =
this.props;
const handleIpfsGatewaySave = (gateway) => {
const url = gateway ? new URL(addUrlProtocolPrefix(gateway)) : '';
const { host } = url;
this.props.setIpfsGateway(host);
};
const handleIpfsGatewayChange = (url) => { const handleIpfsGatewayChange = (url) => {
this.setState(() => { if (url.length > 0) {
let ipfsError = '';
try { try {
const urlObj = new URL(addUrlProtocolPrefix(url)); const validUrl = addUrlProtocolPrefix(url);
if (!urlObj.host) {
throw new Error(); if (!validUrl) {
ipfsError = t('invalidIpfsGateway');
} }
const urlObj = new URL(validUrl);
// don't allow the use of this gateway // don't allow the use of this gateway
if (urlObj.host === 'gateway.ipfs.io') { if (urlObj.host === 'gateway.ipfs.io') {
throw new Error('Forbidden gateway'); ipfsError = t('forbiddenIpfsGateway');
}
if (ipfsError.length === 0) {
this.props.setIpfsGateway(urlObj.host);
} }
} catch (error) { } catch (error) {
ipfsError = ipfsError = t('invalidIpfsGateway');
error.message === 'Forbidden gateway'
? t('forbiddenIpfsGateway')
: t('invalidIpfsGateway');
} }
} else {
ipfsError = t('invalidIpfsGateway');
}
handleIpfsGatewaySave(url); this.setState({
return { ipfsGateway: url,
ipfsGateway: url, ipfsGatewayError: ipfsError,
ipfsGatewayError: ipfsError,
};
}); });
}; };
const handleIpfsToggle = (url) => {
url?.length < 1
? handleIpfsGatewayChange(IPFS_DEFAULT_GATEWAY_URL)
: handleIpfsGatewayChange('');
};
return ( return (
<Box <Box
ref={this.settingsRefs[6]} ref={this.settingsRefs[6]}
@ -426,27 +415,36 @@ export default class SecurityTab extends PureComponent {
{t('ipfsGatewayDescription')} {t('ipfsGatewayDescription')}
</div> </div>
</div> </div>
<div className="settings-page__content-item-col"> <div
className="settings-page__content-item-col"
data-testid="ipfsToggle"
>
<ToggleButton <ToggleButton
value={this.state.ipfsGateway} value={this.state.ipfsToggle}
onToggle={(value) => { onToggle={(value) => {
handleIpfsToggle(value); if (value) {
this.setState({ ipfsToggle: Boolean(value) }); // turning from true to false
this.props.setIpfsGateway('');
} else {
// turning from false to true
handleIpfsGatewayChange(this.state.ipfsGateway);
}
this.setState({ ipfsToggle: !value });
}} }}
offLabel={t('off')} offLabel={t('off')}
onLabel={t('on')} onLabel={t('on')}
/> />
</div> </div>
{!this.state.ipfsToggle && ( {this.state.ipfsToggle && (
<div className="settings-page__content-item"> <div className="settings-page__content-item">
<span>{t('addIPFSGateway')}</span> <span>{t('addIPFSGateway')}</span>
<div className="settings-page__content-item-col"> <div className="settings-page__content-item-col">
<TextField <TextField
type="text" type="text"
disabled={!this.state.ipfsGateway}
value={this.state.ipfsGateway} value={this.state.ipfsGateway}
onChange={(e) => handleIpfsGatewayChange(e.target.value)} onChange={(e) => handleIpfsGatewayChange(e.target.value)}
error={ipfsGatewayError} error={this.state.ipfsGatewayError}
fullWidth fullWidth
margin="dense" margin="dense"
/> />
@ -508,8 +506,10 @@ export default class SecurityTab extends PureComponent {
data-testid="ipfs-gateway-resolution-container" data-testid="ipfs-gateway-resolution-container"
> >
<ToggleButton <ToggleButton
value={useAddressBarEnsResolution} value={this.props.useAddressBarEnsResolution}
onToggle={(value) => setUseAddressBarEnsResolution(!value)} onToggle={(value) =>
this.props.setUseAddressBarEnsResolution(!value)
}
offLabel={t('off')} offLabel={t('off')}
onLabel={t('on')} onLabel={t('on')}
/> />
@ -696,31 +696,32 @@ export default class SecurityTab extends PureComponent {
{t('displayNftMediaDescription')} {t('displayNftMediaDescription')}
</div> </div>
</div> </div>
<div className="settings-page__content-item">
<div <div
className="settings-page__content-item-col" className="settings-page__content-item-col"
data-testid="displayNftMedia" data-testid="enableOpenSeaAPI"
> >
<ToggleButton <ToggleButton
value={openSeaEnabled} value={openSeaEnabled}
onToggle={(value) => { onToggle={(value) => {
this.context.trackEvent({ this.context.trackEvent({
category: MetaMetricsEventCategory.Settings, category: MetaMetricsEventCategory.Settings,
event: 'Enabled/Disable OpenSea', event: 'Enabled/Disable OpenSea',
properties: { properties: {
action: 'Enabled/Disable OpenSea', action: 'Enabled/Disable OpenSea',
legacy_event: true, legacy_event: true,
}, },
}); });
// value is positive when being toggled off // value is positive when being toggled off
if (value && useNftDetection) { if (value && useNftDetection) {
setUseNftDetection(false); setUseNftDetection(false);
} }
setOpenSeaEnabled(!value); setOpenSeaEnabled(!value);
}} }}
offLabel={t('off')} offLabel={t('off')}
onLabel={t('on')} onLabel={t('on')}
/> />
</div>
</div> </div>
</Box> </Box>
); );

View File

@ -1,8 +1,12 @@
import { fireEvent, queryByRole, screen } from '@testing-library/react'; import { fireEvent, queryByRole, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import configureMockStore from 'redux-mock-store'; import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
import mockState from '../../../../test/data/mock-state.json'; import mockState from '../../../../test/data/mock-state.json';
import { tEn } from '../../../../test/lib/i18n-helpers';
import { renderWithProvider } from '../../../../test/lib/render-helpers'; import { renderWithProvider } from '../../../../test/lib/render-helpers';
import SecurityTab from './security-tab.container'; import SecurityTab from './security-tab.container';
@ -21,8 +25,10 @@ describe('Security Tab', () => {
const mockStore = configureMockStore([thunk])(mockState); const mockStore = configureMockStore([thunk])(mockState);
function toggleCheckbox(testId, initialState) { function toggleCheckbox(testId, initialState, skipRender = false) {
renderWithProvider(<SecurityTab />, mockStore); if (!skipRender) {
renderWithProvider(<SecurityTab />, mockStore);
}
const container = screen.getByTestId(testId); const container = screen.getByTestId(testId);
const checkbox = queryByRole(container, 'checkbox'); const checkbox = queryByRole(container, 'checkbox');
@ -46,12 +52,31 @@ describe('Security Tab', () => {
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it('toggles Display NFT media enabled', async () => { it('toggles opensea api enabled off', async () => {
expect(await toggleCheckbox('displayNftMedia', false)).toBe(true); expect(await toggleCheckbox('enableOpenSeaAPI', true)).toBe(true);
});
it('toggles opensea api enabled on', async () => {
mockState.metamask.openSeaEnabled = false;
const localMockStore = configureMockStore([thunk])(mockState);
renderWithProvider(<SecurityTab />, localMockStore);
expect(await toggleCheckbox('enableOpenSeaAPI', false, true)).toBe(true);
}); });
it('toggles nft detection', async () => { it('toggles nft detection', async () => {
expect(await toggleCheckbox('useNftDetection', false)).toBe(true); expect(await toggleCheckbox('useNftDetection', true)).toBe(true);
});
it('toggles nft detection from another initial state', async () => {
mockState.metamask.openSeaEnabled = false;
mockState.metamask.useNftDetection = false;
const localMockStore = configureMockStore([thunk])(mockState);
renderWithProvider(<SecurityTab />, localMockStore);
expect(await toggleCheckbox('useNftDetection', false, true)).toBe(true);
}); });
it('toggles phishing detection', async () => { it('toggles phishing detection', async () => {
@ -103,4 +128,81 @@ describe('Security Tab', () => {
screen.queryByTestId(`srp_stage_introduction`), screen.queryByTestId(`srp_stage_introduction`),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it('sets IPFS gateway', async () => {
const user = userEvent.setup();
renderWithProvider(<SecurityTab />, mockStore);
const ipfsField = screen.getByDisplayValue(mockState.metamask.ipfsGateway);
await user.click(ipfsField);
await userEvent.clear(ipfsField);
expect(ipfsField).toHaveValue('');
expect(screen.queryByText(tEn('invalidIpfsGateway'))).toBeInTheDocument();
expect(
screen.queryByText(tEn('forbiddenIpfsGateway')),
).not.toBeInTheDocument();
await userEvent.type(ipfsField, 'https://');
expect(ipfsField).toHaveValue('https://');
expect(screen.queryByText(tEn('invalidIpfsGateway'))).toBeInTheDocument();
expect(
screen.queryByText(tEn('forbiddenIpfsGateway')),
).not.toBeInTheDocument();
await userEvent.type(ipfsField, '//');
expect(ipfsField).toHaveValue('https:////');
expect(screen.queryByText(tEn('invalidIpfsGateway'))).toBeInTheDocument();
expect(
screen.queryByText(tEn('forbiddenIpfsGateway')),
).not.toBeInTheDocument();
await userEvent.clear(ipfsField);
await userEvent.type(ipfsField, 'gateway.ipfs.io');
expect(ipfsField).toHaveValue('gateway.ipfs.io');
expect(
screen.queryByText(tEn('invalidIpfsGateway')),
).not.toBeInTheDocument();
expect(screen.queryByText(tEn('forbiddenIpfsGateway'))).toBeInTheDocument();
});
it('toggles IPFS gateway', async () => {
mockState.metamask.ipfsGateway = '';
const localMockStore = configureMockStore([thunk])(mockState);
renderWithProvider(<SecurityTab />, localMockStore);
expect(await toggleCheckbox('ipfsToggle', false, true)).toBe(true);
expect(await toggleCheckbox('ipfsToggle', true, true)).toBe(true);
});
it('toggles ENS domains in address bar', async () => {
expect(
await toggleCheckbox('ipfs-gateway-resolution-container', false),
).toBe(true);
});
it('clicks "Add Custom Network"', async () => {
const user = userEvent.setup();
renderWithProvider(<SecurityTab />, mockStore);
// Test the default path where `getEnvironmentType() === undefined`
await user.click(screen.getByText(tEn('addCustomNetwork')));
// Now force it down the path where `getEnvironmentType() === ENVIRONMENT_TYPE_POPUP`
jest
.mocked(getEnvironmentType)
.mockImplementationOnce(() => ENVIRONMENT_TYPE_POPUP);
global.platform = { openExtensionInBrowser: jest.fn() };
await user.click(screen.getByText(tEn('addCustomNetwork')));
expect(global.platform.openExtensionInBrowser).toHaveBeenCalled();
});
}); });

View File

@ -24494,7 +24494,6 @@ __metadata:
typescript: "npm:~4.4.0" typescript: "npm:~4.4.0"
unicode-confusables: "npm:^0.1.1" unicode-confusables: "npm:^0.1.1"
uuid: "npm:^8.3.2" uuid: "npm:^8.3.2"
valid-url: "npm:^1.0.9"
vinyl: "npm:^2.2.1" vinyl: "npm:^2.2.1"
vinyl-buffer: "npm:^1.0.1" vinyl-buffer: "npm:^1.0.1"
vinyl-source-stream: "npm:^2.0.0" vinyl-source-stream: "npm:^2.0.0"
@ -34288,13 +34287,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"valid-url@npm:^1.0.9":
version: 1.0.9
resolution: "valid-url@npm:1.0.9"
checksum: 343dfaf85eb3691dc8eb93f7bc007be1ee6091e6c6d1a68bf633cb85e4bf2930e34ca9214fb2c3330de5b652510b257a8ee1ff0a0a37df0925e9dabf93ee512d
languageName: node
linkType: hard
"validate-npm-package-license@npm:^3.0.1": "validate-npm-package-license@npm:^3.0.1":
version: 3.0.4 version: 3.0.4
resolution: "validate-npm-package-license@npm:3.0.4" resolution: "validate-npm-package-license@npm:3.0.4"