diff --git a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js index 7c0c5af57..f7c2d586c 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/add-ethereum-chain.js @@ -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 { errorCodes, ethErrors } from 'eth-rpc-errors'; +import { omit } from 'lodash'; import { MESSAGE_TYPE, UNKNOWN_TICKER_SYMBOL, } from '../../../../../shared/constants/app'; +import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics'; import { isPrefixedFormattedHexString, isSafeChainId, } from '../../../../../shared/modules/network.utils'; -import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics'; +import { getValidUrl } from '../../util'; const addEthereumChain = { methodNames: [MESSAGE_TYPE.ADD_ETHEREUM_CHAIN], @@ -83,27 +83,25 @@ async function addEthereumChainHandler( ); } - const isLocalhost = (strUrl) => { - try { - const url = new URL(strUrl); - return url.hostname === 'localhost' || url.hostname === '127.0.0.1'; - } catch (error) { - return false; - } - }; + function isLocalhostOrHttps(urlString) { + const url = getValidUrl(urlString); + + return ( + url !== null && + (url.hostname === 'localhost' || + url.hostname === '127.0.0.1' || + url.protocol === 'https:') + ); + } const firstValidRPCUrl = Array.isArray(rpcUrls) - ? rpcUrls.find( - (rpcUrl) => isLocalhost(rpcUrl) || validUrl.isHttpsUri(rpcUrl), - ) + ? rpcUrls.find((rpcUrl) => isLocalhostOrHttps(rpcUrl)) : null; const firstValidBlockExplorerUrl = blockExplorerUrls !== null && Array.isArray(blockExplorerUrls) - ? blockExplorerUrls.find( - (blockExplorerUrl) => - isLocalhost(blockExplorerUrl) || - validUrl.isHttpsUri(blockExplorerUrl), + ? blockExplorerUrls.find((blockExplorerUrl) => + isLocalhostOrHttps(blockExplorerUrl), ) : null; diff --git a/app/scripts/lib/util.test.js b/app/scripts/lib/util.test.js index c93e9f8e0..3b70d9be7 100644 --- a/app/scripts/lib/util.test.js +++ b/app/scripts/lib/util.test.js @@ -1,24 +1,27 @@ -import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils'; import { - ENVIRONMENT_TYPE_POPUP, - ENVIRONMENT_TYPE_NOTIFICATION, - ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_BACKGROUND, - PLATFORM_FIREFOX, - PLATFORM_OPERA, + ENVIRONMENT_TYPE_FULLSCREEN, + ENVIRONMENT_TYPE_NOTIFICATION, + ENVIRONMENT_TYPE_POPUP, PLATFORM_CHROME, PLATFORM_EDGE, + PLATFORM_FIREFOX, + PLATFORM_OPERA, } from '../../../shared/constants/app'; import { + TransactionEnvelopeType, TransactionStatus, TransactionType, - TransactionEnvelopeType, } from '../../../shared/constants/transaction'; +import { isPrefixedFormattedHexString } from '../../../shared/modules/network.utils'; import { + addUrlProtocolPrefix, deferredPromise, + formatTxMetaForRpcResult, getEnvironmentType, getPlatform, - formatTxMetaForRpcResult, + getValidUrl, + isWebUrl, } from './util'; 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', () => { it('should return true for valid hex strings', () => { expect(isPrefixedFormattedHexString('0x1')).toStrictEqual(true); diff --git a/app/scripts/lib/util.ts b/app/scripts/lib/util.ts index dc21a9608..a6d159bf4 100644 --- a/app/scripts/lib/util.ts +++ b/app/scripts/lib/util.ts @@ -1,24 +1,24 @@ +import urlLib from 'url'; +import { AccessList } from '@ethereumjs/tx'; import BN from 'bn.js'; import { memoize } from 'lodash'; -import { AccessList } from '@ethereumjs/tx'; -import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network'; - import { - ENVIRONMENT_TYPE_POPUP, - ENVIRONMENT_TYPE_NOTIFICATION, - ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_BACKGROUND, - PLATFORM_FIREFOX, - PLATFORM_OPERA, + ENVIRONMENT_TYPE_FULLSCREEN, + ENVIRONMENT_TYPE_NOTIFICATION, + ENVIRONMENT_TYPE_POPUP, + PLATFORM_BRAVE, PLATFORM_CHROME, PLATFORM_EDGE, - PLATFORM_BRAVE, + PLATFORM_FIREFOX, + PLATFORM_OPERA, } from '../../../shared/constants/app'; -import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; +import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network'; import { TransactionEnvelopeType, TransactionMeta, } from '../../../shared/constants/transaction'; +import { stripHexPrefix } from '../../../shared/modules/hexstring-utils'; /** * @see {@link getEnvironmentType} @@ -143,13 +143,13 @@ function checkAlarmExists(alarmList: { name: string }[], alarmName: string) { } export { - getPlatform, - getEnvironmentType, - hexToBn, BnMultiplyByFraction, addHexPrefix, - getChainType, checkAlarmExists, + getChainType, + getEnvironmentType, + getPlatform, + hexToBn, }; // Taken from https://stackoverflow.com/a/1349426/3696652 @@ -235,10 +235,43 @@ export function previousValueComparator( } export function addUrlProtocolPrefix(urlString: string) { - if (!urlString.match(/(^http:\/\/)|(^https:\/\/)/u)) { - return `https://${urlString}`; + let trimmed = urlString.trim(); + + 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 { diff --git a/jest.config.js b/jest.config.js index c95ec4ce8..1b68e475a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,7 @@ module.exports = { '/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.ts', '/app/scripts/controllers/transactions/IncomingTransactionHelper.ts', '/app/scripts/flask/**/*.js', - '/app/scripts/lib/**/*.js', + '/app/scripts/lib/**/*.(js|ts)', '/app/scripts/lib/createRPCMethodTrackingMiddleware.js', '/app/scripts/migrations/*.js', '/app/scripts/migrations/*.ts', @@ -48,8 +48,7 @@ module.exports = { '/app/scripts/controllers/sign.test.ts', '/app/scripts/controllers/decrypt-message.test.ts', '/app/scripts/flask/**/*.test.js', - '/app/scripts/lib/**/*.test.js', - '/app/scripts/lib/**/*.test.ts', + '/app/scripts/lib/**/*.test.(js|ts)', '/app/scripts/lib/createRPCMethodTrackingMiddleware.test.js', '/app/scripts/migrations/*.test.(js|ts)', '/app/scripts/platforms/*.test.js', diff --git a/package.json b/package.json index b5650934e..d5f482941 100644 --- a/package.json +++ b/package.json @@ -362,7 +362,6 @@ "single-call-balance-checker-abi": "^1.0.0", "unicode-confusables": "^0.1.1", "uuid": "^8.3.2", - "valid-url": "^1.0.9", "web3-stream-provider": "^4.0.0", "zxcvbn": "^4.4.2" }, diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 69db1fc05..ae58d5dba 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -340,6 +340,8 @@ "unapprovedTypedMessagesCount": 0, "useTokenDetection": true, "useCurrencyRateCheck": true, + "useNftDetection": true, + "openSeaEnabled": true, "advancedGasFee": { "maxBaseFee": "75", "priorityFee": "2" diff --git a/ui/pages/keychains/reveal-seed.js b/ui/pages/keychains/reveal-seed.js index 3dec64888..c2f0125fb 100644 --- a/ui/pages/keychains/reveal-seed.js +++ b/ui/pages/keychains/reveal-seed.js @@ -129,7 +129,7 @@ const RevealSeedPage = () => { const renderPasswordPromptContent = () => { return ( -
handleSubmit(event)}> + { return prefixedChainId; }; -const isValidWhenAppended = (url) => { - const appendedRpc = `http://${url}`; - return validUrl.isWebUri(appendedRpc) && !url.match(/^https?:\/\/$/u); -}; - const NetworksForm = ({ addNewNetwork, restrictHeight, @@ -208,23 +203,20 @@ const NetworksForm = ({ const validateBlockExplorerURL = useCallback( (url) => { - if (!validUrl.isWebUri(url) && url !== '') { - let errorKey; - let errorMessage; - - if (isValidWhenAppended(url)) { - errorKey = 'urlErrorMsg'; - errorMessage = t('urlErrorMsg'); - } else { - errorKey = 'invalidBlockExplorerURL'; - errorMessage = t('invalidBlockExplorerURL'); + if (url.length > 0 && !isWebUrl(url)) { + if (isWebUrl(`https://${url}`)) { + return { + key: 'urlErrorMsg', + msg: t('urlErrorMsg'), + }; } return { - key: errorKey, - msg: errorMessage, + key: 'invalidBlockExplorerURL', + msg: t('invalidBlockExplorerURL'), }; } + return null; }, [t], @@ -407,7 +399,6 @@ const NetworksForm = ({ const validateRPCUrl = useCallback( (url) => { - const isValidUrl = validUrl.isWebUri(url); const [ { rpcUrl: matchingRPCUrl = null, @@ -417,20 +408,16 @@ const NetworksForm = ({ ] = networksToRender.filter((e) => e.rpcUrl === url); const { rpcUrl: selectedNetworkRpcUrl } = selectedNetwork; - if (!isValidUrl && url !== '') { - let errorKey; - let errorMessage; - if (isValidWhenAppended(url)) { - errorKey = 'urlErrorMsg'; - errorMessage = t('urlErrorMsg'); - } else { - errorKey = 'invalidRPC'; - errorMessage = t('invalidRPC'); + if (url.length > 0 && !isWebUrl(url)) { + if (isWebUrl(`https://${url}`)) { + return { + key: 'urlErrorMsg', + msg: t('urlErrorMsg'), + }; } - return { - key: errorKey, - msg: errorMessage, + key: 'invalidRPC', + msg: t('invalidRPC'), }; } else if (matchingRPCUrl && matchingRPCUrl !== selectedNetworkRpcUrl) { return { diff --git a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap index 85b5b4e77..f4b8627f9 100644 --- a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap +++ b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap @@ -448,6 +448,7 @@ exports[`Security Tab should match snapshot 1`] = `
-