From 056f9217c074538935ea1dbe9586408578320cb3 Mon Sep 17 00:00:00 2001 From: Marco Rodolfi Date: Wed, 13 Mar 2024 15:17:15 +0100 Subject: [PATCH] Initial implementation to warn users of potential dangers from third party repositories and plugins --- backend/locales/en-US.json | 9 ++ .../src/components/modals/WarnThirdParty.tsx | 82 +++++++++++++++++++ .../settings/pages/developer/index.tsx | 17 +++- .../settings/pages/general/StoreSelect.tsx | 38 +++++---- frontend/src/utils/TranslationHelper.tsx | 23 ++++++ 5 files changed, 153 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/modals/WarnThirdParty.tsx diff --git a/backend/locales/en-US.json b/backend/locales/en-US.json index 1d0b9f774..693ea6cb6 100644 --- a/backend/locales/en-US.json +++ b/backend/locales/en-US.json @@ -264,6 +264,15 @@ "updating": "Updating" } }, + "WarnThirdParty":{ + "title_zip": "Third party plugin install", + "title_repo": "Third party store selection", + "button_processing_one": "Please wait {{timer}} second", + "button_processing_many": "Please wait {{timer}} seconds", + "button_idle": "I accept the risks", + "desc_zip": "This plugin that has been requested to be installed is taken outside our official store; we haven't audited it, so it will contain unvetted code: it might be doing exactly what it would be supposed to do or it might have additional features built in that will try to steal your data and take over your Steam account. Decky devs are not responsible for any problem arising from installing this plugin.", + "desc_repo": "This third party store that has been requested to be used is not our official store; we haven't audited it, so it will contain plugins with unvetted code from us: you should check with whoever is maintaining this store and decide for yourself if you trust them or not. Decky devs are not responsible for any problem arising from installing plugins from this third party store." + }, "Testing": { "download": "Download" } diff --git a/frontend/src/components/modals/WarnThirdParty.tsx b/frontend/src/components/modals/WarnThirdParty.tsx new file mode 100644 index 000000000..d6adf6abb --- /dev/null +++ b/frontend/src/components/modals/WarnThirdParty.tsx @@ -0,0 +1,82 @@ +import { ConfirmModal } from 'decky-frontend-lib'; +import { FC, useEffect, useState } from 'react'; +import { FaExclamationTriangle } from 'react-icons/fa'; + +import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper'; + +interface WarnThirdPartyProps { + seconds?: number; + type: WarnThirdPartyType; + onOK(): void; + onCancel(): void; + closeModal?(): void; +} + +export enum WarnThirdPartyType { + REPO = 0, + ZIP = 1, +} + +const WarnThirdParty: FC = ({ seconds = 5, type, onOK, onCancel, closeModal }) => { + const [waitTimer, setWaitTimer] = useState(seconds); + + useEffect(() => { + // exit early when we reach 0 + if (waitTimer <= 0) return; + + // save intervalId to clear the interval when the + // component re-renders + const intervalId = setInterval(() => { + setWaitTimer(waitTimer - 1); + }, 1000); + + // clear interval on re-render to avoid memory leaks + return () => clearInterval(intervalId); + // add waitTimer as a dependency to re-rerun the effect + // when we update it + }, [waitTimer]); + + return ( + 0} + closeModal={closeModal} + onOK={async () => { + await onOK(); + }} + onCancel={async () => { + await onCancel(); + }} + strTitle={ +
+ + +
+ } + strOKButtonText={ + waitTimer > 0 ? ( +
+ +
+ ) : ( +
+ +
+ ) + } + > + +
+ +
+
+
+ ); +}; + +export default WarnThirdParty; diff --git a/frontend/src/components/settings/pages/developer/index.tsx b/frontend/src/components/settings/pages/developer/index.tsx index 5ed765155..b6e5c18fe 100644 --- a/frontend/src/components/settings/pages/developer/index.tsx +++ b/frontend/src/components/settings/pages/developer/index.tsx @@ -7,6 +7,7 @@ import { Navigation, TextField, Toggle, + showModal, } from 'decky-frontend-lib'; import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,6 +19,7 @@ import { installFromURL } from '../../../../store'; import { useSetting } from '../../../../utils/hooks/useSetting'; import { getSetting } from '../../../../utils/settings'; import { FileSelectionType } from '../../../modals/filepicker'; +import WarnThirdParty, { WarnThirdPartyType } from '../../../modals/WarnThirdParty'; import RemoteDebuggingSettings from '../general/RemoteDebugging'; const logger = new Logger('DeveloperIndex'); @@ -77,7 +79,20 @@ export default function DeveloperSettings() { } icon={} > - installFromURL(pluginURL)}> + + showModal( + { + installFromURL(pluginURL); + }} + onCancel={() => {}} + />, + ) + } + > {t('SettingsDeveloperIndex.third_party_plugins.button_install')} diff --git a/frontend/src/components/settings/pages/general/StoreSelect.tsx b/frontend/src/components/settings/pages/general/StoreSelect.tsx index 3cb80303e..4a04638c0 100644 --- a/frontend/src/components/settings/pages/general/StoreSelect.tsx +++ b/frontend/src/components/settings/pages/general/StoreSelect.tsx @@ -1,4 +1,4 @@ -import { Dropdown, Field, TextField } from 'decky-frontend-lib'; +import { Dropdown, Field, TextField, showModal } from 'decky-frontend-lib'; import { FunctionComponent } from 'react'; import { useTranslation } from 'react-i18next'; import { FaShapes } from 'react-icons/fa'; @@ -6,6 +6,7 @@ import { FaShapes } from 'react-icons/fa'; import Logger from '../../../../logger'; import { Store } from '../../../../store'; import { useSetting } from '../../../../utils/hooks/useSetting'; +import WarnThirdParty, { WarnThirdPartyType } from '../../../modals/WarnThirdParty'; const logger = new Logger('StoreSelect'); @@ -38,20 +39,27 @@ const StoreSelect: FunctionComponent<{}> = () => { }} /> - {selectedStore == Store.Custom && ( - setSelectedStoreURL(e?.target.value || null)} - /> - } - icon={} - > - )} + {selectedStore == Store.Custom && + showModal( + {}} + onCancel={() => setSelectedStore(Store.Default)} + />, + ) && ( + setSelectedStoreURL(e?.target.value || null)} + /> + } + icon={} + > + )} ); }; diff --git a/frontend/src/utils/TranslationHelper.tsx b/frontend/src/utils/TranslationHelper.tsx index 99584d6be..95dcd7afb 100644 --- a/frontend/src/utils/TranslationHelper.tsx +++ b/frontend/src/utils/TranslationHelper.tsx @@ -1,6 +1,7 @@ import { FC } from 'react'; import { Translation } from 'react-i18next'; +import { WarnThirdPartyType } from '../components/modals/WarnThirdParty'; import Logger from '../logger'; import { InstallType } from '../plugin'; @@ -8,6 +9,7 @@ export enum TranslationClass { PLUGIN_LOADER = 'PluginLoader', PLUGIN_INSTALL_MODAL = 'PluginInstallModal', DEVELOPER = 'Developer', + WARN_THIRD_PARTY = 'WarnThirdParty', } interface TranslationHelperProps { @@ -15,6 +17,7 @@ interface TranslationHelperProps { trans_text: string; i18n_args?: {}; install_type?: number; + warn_type?: WarnThirdPartyType; } const logger = new Logger('TranslationHelper'); @@ -24,6 +27,7 @@ const TranslationHelper: FC = ({ trans_text, i18n_args = null, install_type = 0, + warn_type = WarnThirdPartyType.REPO, }) => { return ( @@ -52,6 +56,25 @@ const TranslationHelper: FC = ({ return i18n_args ? t(TranslationClass.DEVELOPER + '.' + trans_text, i18n_args) : t(TranslationClass.DEVELOPER + '.' + trans_text); + //Handle different messages in different class cases + case TranslationClass.WARN_THIRD_PARTY: + //Needed only for title and description + if (!trans_text.startsWith('button')) { + switch (warn_type) { + case WarnThirdPartyType.REPO: + return i18n_args + ? t(TranslationClass.WARN_THIRD_PARTY + '.' + trans_text + '_repo', i18n_args) + : t(TranslationClass.WARN_THIRD_PARTY + '.' + trans_text + '_repo'); + case WarnThirdPartyType.ZIP: + return i18n_args + ? t(TranslationClass.WARN_THIRD_PARTY + '.' + trans_text + '_zip', i18n_args) + : t(TranslationClass.WARN_THIRD_PARTY + '.' + trans_text + '_zip'); + } + } else { + return i18n_args + ? t(TranslationClass.WARN_THIRD_PARTY + '.' + trans_text, i18n_args) + : t(TranslationClass.WARN_THIRD_PARTY + '.' + trans_text); + } default: logger.error('We should never fall in the default case!'); return '';