diff --git a/public/electron/main.js b/public/electron/main.js index 2456ccc..1fa43cc 100644 --- a/public/electron/main.js +++ b/public/electron/main.js @@ -4,6 +4,7 @@ const { ipcMain, shell, session, + dialog, } = require("electron"); const { getDefaultChromeDataDir } = require("./constants") const os = require("os"); @@ -201,6 +202,22 @@ app.on("ready", async () => { ipcMain.handle("isWindows", (_event) => constants.isWindows); + ipcMain.handle('selectFile', async () => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ['openFile'], + filters: [ + { name: 'XML Files', extensions: ['xml'] }, + { name: 'All Files', extensions: ['*'] } + ] + }); + + if (!result.canceled && result.filePaths.length > 0) { + return result.filePaths[0]; + } else { + return null; + } + }); + await mainReady; mainWindow.webContents.send("appStatus", "ready"); diff --git a/public/electron/preload.js b/public/electron/preload.js index ddb2a63..0c431e8 100644 --- a/public/electron/preload.js +++ b/public/electron/preload.js @@ -128,5 +128,9 @@ contextBridge.exposeInMainWorld("services", { ); return response; }, + selectFile: async () => { + const filePath = await ipcRenderer.invoke('selectFile'); + return filePath; + }, getIsWindows: async () => ipcRenderer.invoke("isWindows"), }); diff --git a/src/MainWindow/HomePage/InitScanForm.jsx b/src/MainWindow/HomePage/InitScanForm.jsx index 1ad9a1d..b0b8c33 100644 --- a/src/MainWindow/HomePage/InitScanForm.jsx +++ b/src/MainWindow/HomePage/InitScanForm.jsx @@ -19,13 +19,15 @@ const InitScanForm = ({ prevUrlErrorMessage, scanButtonIsClicked, setScanButtonIsClicked, - isAbortingScan + isAbortingScan, }) => { const [openPageLimitAdjuster, setOpenPageLimitAdjuster] = useState(false); const [pageWord, setPageWord] = useState("pages"); const pageLimitAdjuster = useRef(); const scanTypeOptions = Object.keys(scanTypes); const fileTypesOptions = Object.keys(fileTypes); + const [selectedFile, setSelectedFile] = useState(null); // State for selected file + const [fileUrl, setFileUrl] = useState(""); // State for the file URL if (isProxy) { delete viewportTypes.specific; @@ -34,20 +36,31 @@ const InitScanForm = ({ const viewportOptions = viewportTypes; const deviceOptions = isProxy ? [] : Object.keys(devices); - const cachedPageLimit = sessionStorage.getItem('pageLimit'); - const cachedAdvancedOptions = sessionStorage.getItem('advancedOptions'); - const cachedScanUrl = sessionStorage.getItem('scanUrl'); + const cachedPageLimit = sessionStorage.getItem("pageLimit"); + const cachedAdvancedOptions = sessionStorage.getItem("advancedOptions"); + const cachedScanUrl = sessionStorage.getItem("scanUrl"); const [pageLimit, setPageLimit] = useState(() => { - return cachedPageLimit? JSON.parse(cachedPageLimit) : "100" + return cachedPageLimit ? JSON.parse(cachedPageLimit) : "100"; }); const [advancedOptions, setAdvancedOptions] = useState(() => { - return cachedAdvancedOptions? JSON.parse(cachedAdvancedOptions) : getDefaultAdvancedOptions(isProxy) + return cachedAdvancedOptions + ? JSON.parse(cachedAdvancedOptions) + : getDefaultAdvancedOptions(isProxy); }); - const [scanUrl, setScanUrl] = useState(() => { - return cachedScanUrl? JSON.parse(cachedScanUrl) : "https://" - }); + const [scanUrl, setScanUrl] = useState("") ; + + useEffect(() => { + const cachedScanUrl = sessionStorage.getItem("scanUrl"); + if (cachedScanUrl) { + setScanUrl(JSON.parse(cachedScanUrl)); + } else if (advancedOptions.scanType === scanTypeOptions[3]) { + setScanUrl("file:///"); + } else { + setScanUrl("https://"); + } + }, [advancedOptions.scanType]); useEffect(() => { const urlBarElem = document.getElementById("url-bar"); @@ -57,7 +70,7 @@ const InitScanForm = ({ }, [scanButtonIsClicked, prevUrlErrorMessage]); useEffect(() => { - setPageWord(pageLimit === "1" ? 'page' : 'pages') + setPageWord(pageLimit === "1" ? "page" : "pages"); }, [pageLimit]); const togglePageLimitAdjuster = (e) => { @@ -72,19 +85,20 @@ const InitScanForm = ({ }; const handleScanButtonClicked = () => { - // If chosen device is Mobile in proxy environment, we override the default "Mobile" - // sent to cli.js with iPhone's width 414px - // Prevents the user-agent from triggering in cli.js if (isProxy && advancedOptions.viewport === viewportTypes.mobile) { advancedOptions.viewport = viewportTypes.custom; advancedOptions.viewportWidth = 414; } setScanButtonIsClicked(true); - sessionStorage.setItem('pageLimit', JSON.stringify(pageLimit)); - sessionStorage.setItem('advancedOptions', JSON.stringify(advancedOptions)); - sessionStorage.setItem('scanUrl', JSON.stringify(scanUrl)); - startScan({ scanUrl: scanUrl.trim(), pageLimit, ...advancedOptions }); + sessionStorage.setItem("pageLimit", JSON.stringify(pageLimit)); + sessionStorage.setItem("advancedOptions", JSON.stringify(advancedOptions)); + sessionStorage.setItem("scanUrl", JSON.stringify(scanUrl)); + if (advancedOptions.scanType === scanTypeOptions[3] && selectedFile) { + startScan({ file: selectedFile, scanUrl, pageLimit, ...advancedOptions }); + } else { + startScan({ scanUrl: scanUrl.trim(), pageLimit, ...advancedOptions }); + } }; // styles are in HomePage.scss @@ -100,7 +114,33 @@ const InitScanForm = ({ type="text" value={scanUrl} onChange={(e) => setScanUrl(e.target.value)} + style={{ + display: + advancedOptions.scanType === scanTypeOptions[3] + ? "none" + : "block", + }} // Hide URL input for file scan /> + {advancedOptions.scanType === scanTypeOptions[3] && ( +
+ { + const file = e.target.files[0]; + if (file) { + setScanUrl("file://" + file.path); + } + }} + /> + +
+ )} + {advancedOptions.scanType !== scanTypeOptions[2] && advancedOptions.scanType !== scanTypeOptions[3] && (
@@ -153,7 +193,11 @@ const InitScanForm = ({ onClick={handleScanButtonClicked} disabled={scanButtonIsClicked || isAbortingScan} > - {scanButtonIsClicked || isAbortingScan ? : "Scan"} + {scanButtonIsClicked || isAbortingScan ? ( + + ) : ( + "Scan" + )}
{prevUrlErrorMessage && ( diff --git a/src/MainWindow/HomePage/index.jsx b/src/MainWindow/HomePage/index.jsx index de3c7ea..977d1b5 100644 --- a/src/MainWindow/HomePage/index.jsx +++ b/src/MainWindow/HomePage/index.jsx @@ -8,7 +8,13 @@ import labModeOn from "../../assets/lab-icon-on.svg"; import InitScanForm from "./InitScanForm"; import "./HomePage.scss"; import services from "../../services"; -import { cliErrorCodes, cliErrorTypes, errorStates, versionComparator, urlWithoutAuth } from "../../common/constants"; +import { + cliErrorCodes, + cliErrorTypes, + errorStates, + versionComparator, + urlWithoutAuth, +} from "../../common/constants"; import Modal from "../../common/components/Modal"; import { BasicAuthForm, BasicAuthFormFooter } from "./BasicAuthForm"; import EditUserDetailsModal from "./EditUserDetailsModal"; @@ -19,9 +25,9 @@ import AboutModal from "./AboutModal"; const HomePage = ({ isProxy, appVersionInfo, setCompletedScanId }) => { const navigate = useNavigate(); - const [prevUrlErrorMessage, setPrevUrlErrorMessage] = useState(''); + const [prevUrlErrorMessage, setPrevUrlErrorMessage] = useState(""); const [{ name, email, browser, isLabMode }, setUserData] = useState({ - name: "", + name: "", email: "", browser: null, isLabMode: false, @@ -31,7 +37,7 @@ const HomePage = ({ isProxy, appVersionInfo, setCompletedScanId }) => { const [showNoChromeErrorModal, setShowNoChromeErrorModal] = useState(false); const [showWhatsNewModal, setShowWhatsNewModal] = useState(false); const [showAboutPhModal, setShowAboutPhModal] = useState(false); - const [url, setUrl] = useState(''); + const [url, setUrl] = useState(""); const [scanButtonIsClicked, setScanButtonIsClicked] = useState(false); const [isAbortingScan, setIsAbortingScan] = useState(false); @@ -48,29 +54,28 @@ const HomePage = ({ isProxy, appVersionInfo, setCompletedScanId }) => { }, []); // function that determines whether version is a prerelease/stable build - const getVersionLabel = useCallback((version) => { - const { - latestVer, - latestVerForLab, - allReleaseTags, - allPreReleaseTags - } = appVersionInfo; - - try { - if (latestVer === version) return 'latest stable build'; - if (latestVerForLab === version) return 'latest pre-release'; - - if (allReleaseTags.includes(version)) { - return 'stable build'; - } else if (allPreReleaseTags.includes(version)) { - return 'pre-release'; - } - } catch (error) { + const getVersionLabel = useCallback( + (version) => { + const { latestVer, latestVerForLab, allReleaseTags, allPreReleaseTags } = + appVersionInfo; + + try { + if (latestVer === version) return "latest stable build"; + if (latestVerForLab === version) return "latest pre-release"; + + if (allReleaseTags.includes(version)) { + return "stable build"; + } else if (allPreReleaseTags.includes(version)) { + return "pre-release"; + } + } catch (error) { console.log("Unable to show version label"); - } - - return undefined; // if cannot be determined, this should not happen - }, [appVersionInfo]); + } + + return undefined; // if cannot be determined, this should not happen + }, + [appVersionInfo] + ); const isLatest = () => { const currVer = appVersionInfo.appVersion; @@ -98,9 +103,9 @@ const HomePage = ({ isProxy, appVersionInfo, setCompletedScanId }) => { useEffect(() => { if (scanButtonIsClicked && prevUrlErrorMessage) { - setPrevUrlErrorMessage(''); + setPrevUrlErrorMessage(""); } - }, [scanButtonIsClicked]) + }, [scanButtonIsClicked]); useEffect(() => { if ( @@ -110,11 +115,12 @@ const HomePage = ({ isProxy, appVersionInfo, setCompletedScanId }) => { setShowBasicAuthModal(true); } - if (prevUrlErrorMessage !== null && - prevUrlErrorMessage.includes("No chrome browser") + if ( + prevUrlErrorMessage !== null && + prevUrlErrorMessage.includes("No chrome browser") ) { setShowNoChromeErrorModal(true); - } + } }, [prevUrlErrorMessage]); useEffect(() => { @@ -125,11 +131,11 @@ const HomePage = ({ isProxy, appVersionInfo, setCompletedScanId }) => { const handleShowModal = () => { setShowWhatsNewModal(!!userData["firstLaunchOnUpdate"] && isLatest()); window.services.editUserData({ firstLaunchOnUpdate: false }); - } + }; const whatsNewModalTimeout = setTimeout( handleShowModal, - !!userData["firstLaunchOnUpdate"] ? 500 : 0, - ) + !!userData["firstLaunchOnUpdate"] ? 500 : 0 + ); return whatsNewModalTimeout; }; @@ -138,20 +144,20 @@ const HomePage = ({ isProxy, appVersionInfo, setCompletedScanId }) => { }, []); const editUserData = (info) => { - setUserData(initData => ({ ...initData, ...info })); + setUserData((initData) => ({ ...initData, ...info })); window.services.editUserData(info); }; useEffect(() => { const checkChromeExists = async () => { const chromeExists = await window.services.checkChromeExistsOnMac(); - + if (!chromeExists) { setShowNoChromeErrorModal(true); } - } + }; checkChromeExists(); - }, []) + }, []); const isValidHttpUrl = (input) => { const regexForUrl = new RegExp("^(http|https):/{2}.+$", "gmi"); @@ -159,9 +165,9 @@ const HomePage = ({ isProxy, appVersionInfo, setCompletedScanId }) => { }; const isValidFilepath = (input) => { - const regexForFilepath = new RegExp("^(file:\/\/).+$", "gmi"); + const regexForFilepath = new RegExp("^(file://).+$", "gmi"); return regexForFilepath.test(input); - } + }; const startScan = async (scanDetails) => { scanDetails.browser = isProxy ? "edge" : browser; @@ -174,10 +180,19 @@ const HomePage = ({ isProxy, appVersionInfo, setCompletedScanId }) => { } if (scanDetails.scanType === "Sitemap crawl") { - if (!isValidHttpUrl(scanDetails.scanUrl) && !isValidFilepath(scanDetails.scanUrl)) { + if ( + !isValidHttpUrl(scanDetails.scanUrl) && + !isValidFilepath(scanDetails.scanUrl) + ) { setScanButtonIsClicked(false); setPrevUrlErrorMessage("Invalid sitemap."); - return; + return; + } + } else if (scanDetails.scanType === "Local file") { + if (!isValidFilepath(scanDetails.scanUrl)) { + setScanButtonIsClicked(false); + setPrevUrlErrorMessage("Invalid FilePath. Please type in file:/// format."); + return; } } else if (!isValidHttpUrl(scanDetails.scanUrl)) { setScanButtonIsClicked(false); @@ -193,38 +208,44 @@ const HomePage = ({ isProxy, appVersionInfo, setCompletedScanId }) => { window.localStorage.setItem("scanDetails", JSON.stringify(scanDetails)); - const checkUrlResponse = await services.validateUrlConnectivity(scanDetails); + const checkUrlResponse = await services.validateUrlConnectivity( + scanDetails + ); if (checkUrlResponse.success) { - navigate('/scanning', { state: { url: urlWithoutAuth(scanDetails.scanUrl).toString() } }); - const scanResponse = await services.startScan(scanDetails); + navigate("/scanning", { + state: { url: urlWithoutAuth(scanDetails.scanUrl).toString() }, + }); + const scanResponse = await services.startScan(scanDetails); - if (scanResponse.cancelled){ - return; - } + if (scanResponse.cancelled) { + return; + } - if (scanResponse.failedToCreateExportDir) { - setPrevUrlErrorMessage('Unable to create download directory'); - return; - } + if (scanResponse.failedToCreateExportDir) { + setPrevUrlErrorMessage("Unable to create download directory"); + return; + } - if (scanResponse.success) { - setCompletedScanId(scanResponse.scanId); - if (scanDetails.scanType === 'Custom flow') { - navigate('/custom_flow', { state: { scanDetails }}) - } else { - navigate("/result"); - } - return; - } else { - /* When no pages were scanned (e.g. out of domain upon redirects when valid URL was entered), + if (scanResponse.success) { + setCompletedScanId(scanResponse.scanId); + if (scanDetails.scanType === "Custom flow") { + navigate("/custom_flow", { state: { scanDetails } }); + } else { + navigate("/result"); + } + return; + } else { + /* When no pages were scanned (e.g. out of domain upon redirects when valid URL was entered), redirects user to error page to going to result page with empty result */ - navigate("/error", { state: { errorState: errorStates.noPagesScannedError,timeOfScan }}); - return; - } + navigate("/error", { + state: { errorState: errorStates.noPagesScannedError, timeOfScan }, + }); + return; + } } else { setScanButtonIsClicked(false); if (checkUrlResponse.failedToCreateExportDir) { - setPrevUrlErrorMessage('Unable to create download directory'); + setPrevUrlErrorMessage("Unable to create download directory"); return; } @@ -245,11 +266,14 @@ const HomePage = ({ isProxy, appVersionInfo, setCompletedScanId }) => { errorMessageToShow = "Invalid sitemap."; break; case cliErrorTypes.browserError: - navigate('/error', { state: { errorState: errorStates.browserError,timeOfScan }}); + navigate("/error", { + state: { errorState: errorStates.browserError, timeOfScan }, + }); return; case cliErrorTypes.systemError: default: - errorMessageToShow = "Something went wrong. Please try again later."; + errorMessageToShow = + "Something went wrong. Please try again later."; } console.log(`status error: ${checkUrlResponse.statusCode}`); setPrevUrlErrorMessage(errorMessageToShow); @@ -341,18 +365,21 @@ const HomePage = ({ isProxy, appVersionInfo, setCompletedScanId }) => { /> )} - {showNoChromeErrorModal && - - } - {showWhatsNewModal && getReleaseNotesOnUpdate(appVersionInfo) && + {showNoChromeErrorModal && ( + + )} + {showWhatsNewModal && getReleaseNotesOnUpdate(appVersionInfo) && ( - } - {showAboutPhModal && + )} + {showAboutPhModal && ( { isLabMode={isLabMode} setIsLabMode={(bool) => editUserData({ isLabMode: bool })} /> - } + )}