diff --git a/apps/client/src/services/attributes.ts b/apps/client/src/services/attributes.ts index 2bff933244..fd8a8b0f10 100644 --- a/apps/client/src/services/attributes.ts +++ b/apps/client/src/services/attributes.ts @@ -168,6 +168,42 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin return false; } +/** + * Toggles whether a dangerous attribute is enabled or not. When an attribute is disabled, its name is prefixed with `disabled:`. + * + * Note that this work for non-dangerous attributes as well. + * + * @param note the note whose attribute to change. + * @param type the type of dangerous attribute (label or relation). + * @param name the name of the dangerous attribute. + * @param willEnable whether to enable or disable the attribute. + * @returns a promise that will resolve when the request to the server completes. + */ +async function toggleDangerousAttribute(note: FNote, type: "label" | "relation", name: string, willEnable: boolean) { + const attr = note.getOwnedAttribute(type, name) ?? note.getOwnedAttribute(type, `disabled:${name}`); + if (!attr) return; + const baseName = getNameWithoutDangerousPrefix(attr.name); + const newName = willEnable ? baseName : `disabled:${baseName}`; + if (newName === attr.name) return; + + // We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically. + if (attr.type === "label") { + await setLabel(note.noteId, newName, attr.value); + } else { + await setRelation(note.noteId, newName, attr.value); + } + await removeAttributeById(note.noteId, attr.attributeId); +} + +/** + * Returns the name of an attribute without the `disabled:` prefix, or the same name if it's not disabled. + * @param name the name of an attribute. + * @returns the name without the `disabled:` prefix. + */ +function getNameWithoutDangerousPrefix(name: string) { + return name.startsWith("disabled:") ? name.substring(9) : name; +} + export default { addLabel, setLabel, @@ -177,5 +213,7 @@ export default { removeAttributeById, removeOwnedLabelByName, removeOwnedRelationByName, - isAffecting + isAffecting, + toggleDangerousAttribute, + getNameWithoutDangerousPrefix }; diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 3656035497..7b38c0dd8d 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1075,7 +1075,9 @@ "url_placeholder": "Enter or paste the website address, for example https://triliumnotes.org", "create_button": "Create Web View", "invalid_url_title": "Invalid address", - "invalid_url_message": "Insert a valid web address, for example https://triliumnotes.org." + "invalid_url_message": "Insert a valid web address, for example https://triliumnotes.org.", + "disabled_description": "This web view was imported from an external source. To help protect you from phishing or malicious content, it isn’t loading automatically. You can enable it if you trust the source.", + "disabled_button_enable": "Enable web view" }, "backend_log": { "refresh": "Refresh" diff --git a/apps/client/src/widgets/layout/ActiveContentBadges.tsx b/apps/client/src/widgets/layout/ActiveContentBadges.tsx index 00099d0370..978fe7ef03 100644 --- a/apps/client/src/widgets/layout/ActiveContentBadges.tsx +++ b/apps/client/src/widgets/layout/ActiveContentBadges.tsx @@ -206,31 +206,15 @@ function ActiveContentToggle({ note, info }: { note: FNote, info: ActiveContentI const attrs = note.getOwnedAttributes() .filter(attr => { if (attr.isInheritable) return false; - const baseName = getNameWithoutPrefix(attr.name); + const baseName = attributes.getNameWithoutDangerousPrefix(attr.name); return DANGEROUS_ATTRIBUTES.some(item => item.name === baseName && item.type === attr.type); }); - for (const attr of attrs) { - const baseName = getNameWithoutPrefix(attr.name); - const newName = willEnable ? baseName : `disabled:${baseName}`; - if (newName === attr.name) continue; - - // We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically. - if (attr.type === "label") { - await attributes.setLabel(note.noteId, newName, attr.value); - } else { - await attributes.setRelation(note.noteId, newName, attr.value); - } - await attributes.removeAttributeById(note.noteId, attr.attributeId); - } + await Promise.all(attrs.map(a => attributes.toggleDangerousAttribute(note, a.type, a.name, willEnable))); }} />; } -function getNameWithoutPrefix(name: string) { - return name.startsWith("disabled:") ? name.substring(9) : name; -} - function useActiveContentInfo(note: FNote | null | undefined) { const [ info, setInfo ] = useState(null); diff --git a/apps/client/src/widgets/type_widgets/WebView.css b/apps/client/src/widgets/type_widgets/WebView.css index c158e3ceae..ceeb5ba7af 100644 --- a/apps/client/src/widgets/type_widgets/WebView.css +++ b/apps/client/src/widgets/type_widgets/WebView.css @@ -32,4 +32,8 @@ width: 100%; max-width: 600px; } -} \ No newline at end of file + + .tn-link { + margin-top: 1em; + } +} diff --git a/apps/client/src/widgets/type_widgets/WebView.tsx b/apps/client/src/widgets/type_widgets/WebView.tsx index 40c5ef8655..aeb56d70b2 100644 --- a/apps/client/src/widgets/type_widgets/WebView.tsx +++ b/apps/client/src/widgets/type_widgets/WebView.tsx @@ -3,23 +3,32 @@ import "./WebView.css"; import { useCallback, useState } from "preact/hooks"; import FNote from "../../entities/fnote"; +import attributes from "../../services/attributes"; import { t } from "../../services/i18n"; import toast from "../../services/toast"; -import utils from "../../services/utils"; +import utils, { openInAppHelpFromUrl } from "../../services/utils"; import Button from "../react/Button"; import FormGroup from "../react/FormGroup"; +import FormTextBox from "../react/FormTextBox"; import { useNoteLabel } from "../react/hooks"; +import LinkButton from "../react/LinkButton"; import { TypeWidgetProps } from "./type_widget"; const isElectron = utils.isElectron(); export default function WebView({ note }: TypeWidgetProps) { const [ webViewSrc ] = useNoteLabel(note, "webViewSrc"); + const [ disabledWebViewSrc ] = useNoteLabel(note, "disabled:webViewSrc"); - return (webViewSrc - ? - : - ); + if (disabledWebViewSrc) { + return ; + } + + if (!webViewSrc) { + return ; + } + + return ; } function WebViewContent({ src }: { src: string }) { @@ -48,24 +57,56 @@ function SetupWebView({note}: {note: FNote}) { setSrcLabel(url); }, [ setSrcLabel ]); - return
-
submit(src)}> - + return ( +
+ submit(src)}> + - - {setSrc((e.target as HTMLInputElement)?.value);}} + + {setSrc((e.target as HTMLInputElement)?.value);}} + /> + + +
; + +
+ ); +} + +function DisabledWebView({ note, url }: { note: FNote, url: string }) { + return ( +
+
+ + + + + + +
+ ); } diff --git a/packages/commons/src/lib/attribute_names.ts b/packages/commons/src/lib/attribute_names.ts index da3b890c9c..d5a63de04a 100644 --- a/packages/commons/src/lib/attribute_names.ts +++ b/packages/commons/src/lib/attribute_names.ts @@ -61,6 +61,7 @@ type Labels = { // Note-type specific webViewSrc: string; + "disabled:webViewSrc": string; readOnly: boolean; mapType: string; mapRootNoteId: string;