feat(web_view): add a screen if web view is disabled

This commit is contained in:
Elian Doran 2026-02-15 16:02:51 +02:00
parent 964633f426
commit 4b8d341e00
No known key found for this signature in database
6 changed files with 114 additions and 44 deletions

View File

@ -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
};

View File

@ -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 isnt loading automatically. You can enable it if you trust the source.",
"disabled_button_enable": "Enable web view"
},
"backend_log": {
"refresh": "Refresh"

View File

@ -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<ActiveContentInfo | null>(null);

View File

@ -32,4 +32,8 @@
width: 100%;
max-width: 600px;
}
}
.tn-link {
margin-top: 1em;
}
}

View File

@ -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
? <WebViewContent src={webViewSrc} />
: <SetupWebView note={note} />
);
if (disabledWebViewSrc) {
return <DisabledWebView note={note} url={disabledWebViewSrc} />;
}
if (!webViewSrc) {
return <SetupWebView note={note} />;
}
return <WebViewContent src={webViewSrc} />;
}
function WebViewContent({ src }: { src: string }) {
@ -48,24 +57,56 @@ function SetupWebView({note}: {note: FNote}) {
setSrcLabel(url);
}, [ setSrcLabel ]);
return <div class="web-view-setup-form">
<form class="tn-centered-form" onSubmit={() => submit(src)}>
<span className="bx bx-globe-alt form-icon" />
return (
<div class="web-view-setup-form">
<form class="tn-centered-form" onSubmit={() => submit(src)}>
<span className="bx bx-globe-alt form-icon" />
<FormGroup name="web-view-src-detail" label={t("web_view_setup.title")}>
<input className="form-control"
type="text"
value={src}
placeholder={t("web_view_setup.url_placeholder")}
onChange={(e) => {setSrc((e.target as HTMLInputElement)?.value);}}
<FormGroup name="web-view-src-detail" label={t("web_view_setup.title")}>
<input className="form-control"
type="text"
value={src}
placeholder={t("web_view_setup.url_placeholder")}
onChange={(e) => {setSrc((e.target as HTMLInputElement)?.value);}}
/>
</FormGroup>
<Button
text={t("web_view_setup.create_button")}
primary
keyboardShortcut="Enter"
/>
</FormGroup>
<Button
text={t("web_view_setup.create_button")}
primary
keyboardShortcut="Enter"
/>
</form>
</div>;
</form>
</div>
);
}
function DisabledWebView({ note, url }: { note: FNote, url: string }) {
return (
<div class="web-view-setup-form">
<form class="tn-centered-form">
<span className="bx bx-globe-alt form-icon" />
<FormGroup name="web-view-src-detail" label={t("web_view_setup.disabled_description")}>
<FormTextBox
type="url"
currentValue={url}
disabled
/>
</FormGroup>
<Button
text={t("web_view_setup.disabled_button_enable")}
icon="bx bx-check-shield"
onClick={() => attributes.toggleDangerousAttribute(note, "label", "webViewSrc", true)}
primary
/>
<LinkButton
text="Learn more"
onClick={() => openInAppHelpFromUrl("1vHRoWCEjj0L")}
/>
</form>
</div>
);
}

View File

@ -61,6 +61,7 @@ type Labels = {
// Note-type specific
webViewSrc: string;
"disabled:webViewSrc": string;
readOnly: boolean;
mapType: string;
mapRootNoteId: string;