Add URL scheme whitelist (#3994)

* Add experimental Cursor rules

* Add the ability to customize URL schemes

Closes #3965
This commit is contained in:
Damien Elmes 2025-05-15 15:37:49 +10:00 committed by GitHub
parent f7cdf4eb9e
commit 86c89907e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 121 additions and 26 deletions

7
.cursor/rules/i18n.md Normal file
View file

@ -0,0 +1,7 @@
- We use the fluent system+code generation for translation.
- New strings should be added to rslib/core/. Ask for the appropriate file if you're not sure.
- Assuming a string addons-you-have-count has been added to addons.ftl, that string is accessible in our different languages as follows:
- Python: from aqt.utils import tr; msg = tr.addons_you_have_count(count=3)
- TypeScript: import * as tr from "@generated/ftl"; tr.addonsYouHaveCount({count: 3})
- Rust: collection.tr.addons_you_have_count(3)
- In Qt .ui files, strings that are marked as translatable will automatically use the registered ftl strings. So a QLabel with a title 'addons_you_have_count' that is marked as translatable will automatically use the translation defined in our addons.ftl file.

2
.cursor/rules/testing.md Normal file
View file

@ -0,0 +1,2 @@
- To build and check the project, use ./check(.bat)
- This will format files, then run lints and unit tests.

View file

@ -83,6 +83,13 @@ preferences-ankiweb-intro = AnkiWeb is a free service that lets you keep your fl
preferences-ankihub-intro = AnkiHub provides collaborative deck editing and additional study tools. A paid subscription is required to access certain features.
preferences-third-party-description = Third-party services are unaffiliated with and not endorsed by Anki. Use of these services may require payment.
## URL scheme related
preferences-url-schemes = URL Schemes
preferences-url-scheme-prompt = Allowed { preferences-url-schemes } (space-separated):
preferences-url-scheme-warning = Blocked attempt to open `{ $link }`, which may be a security issue.
If you trust the deck author and wish to proceed, you can add `{ $scheme }` to your allowed { preferences-url-schemes }.
## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.
preferences-basic = Basic

View file

@ -17,7 +17,7 @@
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
<enum>Qt::FocusPolicy::StrongFocus</enum>
</property>
<property name="currentIndex">
<number>0</number>
@ -78,7 +78,7 @@
</sizepolicy>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
<enum>QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
</widget>
</item>
@ -260,7 +260,7 @@
<item>
<spacer name="verticalSpacer_9">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -451,6 +451,13 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="url_schemes">
<property name="text">
<string>preferences_url_schemes</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -466,7 +473,7 @@
<item>
<spacer name="verticalSpacer_12">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -518,10 +525,10 @@
<item>
<spacer name="verticalSpacer_7">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -614,10 +621,10 @@
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
<enum>QSizePolicy::Policy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -739,7 +746,7 @@
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -827,7 +834,7 @@
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -840,7 +847,7 @@
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -918,10 +925,10 @@
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -953,7 +960,7 @@
<item row="1" column="3">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1020,7 +1027,7 @@
<item row="1" column="1">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1035,10 +1042,10 @@
<item>
<spacer name="verticalSpacer_6">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1080,7 +1087,7 @@
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1128,10 +1135,10 @@
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Maximum</enum>
<enum>QSizePolicy::Policy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1207,7 +1214,7 @@
<item>
<spacer name="verticalspacer_13">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -1227,17 +1234,17 @@
<string>preferences_some_settings_will_take_effect_after</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close|QDialogButtonBox::Help</set>
<set>QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::Help</set>
</property>
</widget>
</item>
@ -1266,6 +1273,7 @@
<tabstop>showEstimates</tabstop>
<tabstop>spacebar_rates_card</tabstop>
<tabstop>render_latex</tabstop>
<tabstop>url_schemes</tabstop>
<tabstop>pastePNG</tabstop>
<tabstop>paste_strips_formatting</tabstop>
<tabstop>useCurrent</tabstop>

View file

@ -20,9 +20,11 @@ from aqt.profiles import VideoDriver
from aqt.qt import *
from aqt.sync import sync_login
from aqt.theme import Theme
from aqt.url_schemes import show_url_schemes_dialog
from aqt.utils import (
HelpPage,
add_close_shortcut,
add_ellipsis_to_action_label,
askUser,
disable_help_button,
is_win,
@ -152,6 +154,9 @@ class Preferences(QDialog):
form.monthly_backups.setValue(self.prefs.backups.monthly)
form.minutes_between_backups.setValue(self.prefs.backups.minimum_interval_mins)
add_ellipsis_to_action_label(self.form.url_schemes)
qconnect(self.form.url_schemes.clicked, show_url_schemes_dialog)
def update_collection(self, on_done: Callable[[], None]) -> None:
form = self.form

View file

@ -744,3 +744,9 @@ create table if not exists profiles
def ankihub_username(self) -> str | None:
return self.profile.get("thirdPartyAnkiHubUsername")
def allowed_url_schemes(self) -> list[str]:
return self.profile.get("allowedUrlSchemes", [])
def set_allowed_url_schemes(self, schemes: list[str]) -> None:
self.profile["allowedUrlSchemes"] = schemes

58
qt/aqt/url_schemes.py Normal file
View file

@ -0,0 +1,58 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
from markdown import markdown
from aqt.qt import Qt, QUrl
from aqt.utils import ask_user_dialog, getText, openLink, tr
def show_url_schemes_dialog() -> None:
from aqt import mw
default = " ".join(mw.pm.allowed_url_schemes())
schemes, ok = getText(
prompt=tr.preferences_url_scheme_prompt(),
title=tr.preferences_url_schemes(),
default=default,
)
if ok:
mw.pm.set_allowed_url_schemes(schemes.split(" "))
mw.pm.save()
def is_supported_scheme(url: QUrl) -> bool:
from aqt import mw
scheme = url.scheme().lower()
allowed_schemes = mw.pm.allowed_url_schemes()
return scheme in allowed_schemes or scheme in ["http", "https"]
def open_url_if_supported_scheme(url: QUrl) -> None:
from aqt import mw
if is_supported_scheme(url):
openLink(url)
else:
def on_button(idx: int) -> None:
if idx == 0:
show_url_schemes_dialog()
msg = markdown(
tr.preferences_url_scheme_warning(link=url.toString(), scheme=url.scheme())
)
ask_user_dialog(
msg,
buttons=[
tr.actions_with_ellipsis(action=tr.preferences_url_schemes()),
tr.actions_close(),
],
parent=mw,
callback=on_button,
textFormat=Qt.TextFormat.RichText,
)

View file

@ -1188,7 +1188,7 @@ def disallow_full_screen() -> bool:
)
def add_ellipsis_to_action_label(*actions: QAction) -> None:
def add_ellipsis_to_action_label(*actions: QAction | QPushButton) -> None:
"""Pass actions to add '...' to their labels, indicating that more input is
required before they can be performed.

View file

@ -266,7 +266,9 @@ class AnkiWebPage(QWebEnginePage):
print("onclick handler needs to return false")
return False
# load all other links in browser
openLink(url)
from aqt.url_schemes import open_url_if_supported_scheme
open_url_if_supported_scheme(url)
return False
def _onCmd(self, str: str) -> Any: