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-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. 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. ## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.
preferences-basic = Basic preferences-basic = Basic

View file

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

View file

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

View file

@ -744,3 +744,9 @@ create table if not exists profiles
def ankihub_username(self) -> str | None: def ankihub_username(self) -> str | None:
return self.profile.get("thirdPartyAnkiHubUsername") 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 """Pass actions to add '...' to their labels, indicating that more input is
required before they can be performed. required before they can be performed.

View file

@ -266,7 +266,9 @@ class AnkiWebPage(QWebEnginePage):
print("onclick handler needs to return false") print("onclick handler needs to return false")
return False return False
# load all other links in browser # 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 return False
def _onCmd(self, str: str) -> Any: def _onCmd(self, str: str) -> Any: