Add a preference to toggle LaTeX generation (#3218)

* Add a preference to toggle LaTeX generation

* Fix test

* Remove LaTeX security restrictions

* Show existing LaTeX images regardless of preference

* Lift config check out of loop (dae)

* Shift option to review settings; display warning when disabled (dae)
This commit is contained in:
Abdo 2024-06-01 11:26:28 +03:00 committed by GitHub
parent d981a6e3c6
commit 06f7aa393d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 35 additions and 66 deletions

View File

@ -13,6 +13,8 @@ preferences-media-is-not-backed-up = Media is not backed up. Please create a per
preferences-on-next-sync-force-changes-in = On next sync, force changes in one direction preferences-on-next-sync-force-changes-in = On next sync, force changes in one direction
preferences-paste-clipboard-images-as-png = Paste clipboard images as PNG preferences-paste-clipboard-images-as-png = Paste clipboard images as PNG
preferences-paste-without-shift-key-strips-formatting = Paste without shift key strips formatting preferences-paste-without-shift-key-strips-formatting = Paste without shift key strips formatting
preferences-generate-latex-images-automatically = Generate LaTeX images (security risk)
preferences-latex-generation-disabled = LaTeX image generation is disabled in the preferences.
preferences-periodically-sync-media = Periodically sync media preferences-periodically-sync-media = Periodically sync media
preferences-please-restart-anki-to-complete-language = Please restart Anki to complete language change. preferences-please-restart-anki-to-complete-language = Please restart Anki to complete language change.
preferences-preferences = Preferences preferences-preferences = Preferences

View File

@ -53,6 +53,7 @@ message ConfigKey {
RESET_COUNTS_REVIEWER = 22; RESET_COUNTS_REVIEWER = 22;
RANDOM_ORDER_REPOSITION = 23; RANDOM_ORDER_REPOSITION = 23;
SHIFT_POSITION_OF_EXISTING_CARDS = 24; SHIFT_POSITION_OF_EXISTING_CARDS = 24;
RENDER_LATEX = 25;
} }
enum String { enum String {
SET_DUE_BROWSER = 0; SET_DUE_BROWSER = 0;
@ -121,6 +122,7 @@ message Preferences {
bool paste_strips_formatting = 3; bool paste_strips_formatting = 3;
string default_search_text = 4; string default_search_text = 4;
bool ignore_accents_in_search = 5; bool ignore_accents_in_search = 5;
bool render_latex = 6;
} }
message BackupLimits { message BackupLimits {
uint32 daily = 1; uint32 daily = 1;

View File

@ -5,12 +5,12 @@ from __future__ import annotations
import html import html
import os import os
import re
from dataclasses import dataclass from dataclasses import dataclass
import anki import anki
import anki.collection import anki.collection
from anki import card_rendering_pb2, hooks from anki import card_rendering_pb2, hooks
from anki.config import Config
from anki.models import NotetypeDict from anki.models import NotetypeDict
from anki.template import TemplateRenderContext, TemplateRenderOutput from anki.template import TemplateRenderContext, TemplateRenderOutput
from anki.utils import call, is_mac, namedtmp, tmpdir from anki.utils import call, is_mac, namedtmp, tmpdir
@ -36,9 +36,6 @@ svgCommands = [
["dvisvgm", "--no-fonts", "--exact", "-Z", "2", "tmp.dvi", "-o", "tmp.svg"], ["dvisvgm", "--no-fonts", "--exact", "-Z", "2", "tmp.dvi", "-o", "tmp.svg"],
] ]
# if off, use existing media but don't create new
build = True # pylint: disable=invalid-name
# add standard tex install location to osx # add standard tex install location to osx
if is_mac: if is_mac:
os.environ["PATH"] += ":/usr/texbin:/Library/TeX/texbin" os.environ["PATH"] += ":/usr/texbin:/Library/TeX/texbin"
@ -104,11 +101,15 @@ def render_latex_returning_errors(
out = ExtractedLatexOutput.from_proto(proto) out = ExtractedLatexOutput.from_proto(proto)
errors = [] errors = []
html = out.html html = out.html
render_latex = col.get_config_bool(Config.Bool.RENDER_LATEX)
for latex in out.latex: for latex in out.latex:
# don't need to render? # don't need to render?
if not build or col.media.have(latex.filename): if col.media.have(latex.filename):
continue continue
if not render_latex:
errors.append(col.tr.preferences_latex_generation_disabled())
return html, errors
err = _save_latex_image(col, latex, header, footer, svg) err = _save_latex_image(col, latex, header, footer, svg)
if err is not None: if err is not None:
@ -126,24 +127,6 @@ def _save_latex_image(
) -> str | None: ) -> str | None:
# add header/footer # add header/footer
latex = f"{header}\n{extracted.latex_body}\n{footer}" latex = f"{header}\n{extracted.latex_body}\n{footer}"
# it's only really secure if run in a jail, but these are the most common
tmplatex = latex.replace("\\includegraphics", "")
for bad in (
"\\write18",
"\\readline",
"\\input",
"\\include",
"\\catcode",
"\\openout",
"\\write",
"\\loop",
"\\def",
"\\shipout",
):
# don't mind if the sequence is only part of a command
bad_re = f"\\{bad}[^a-zA-Z]"
if re.search(bad_re, tmplatex):
return col.tr.media_for_security_reasons_is_not(val=bad)
# commands to use # commands to use
if svg: if svg:

View File

@ -6,12 +6,14 @@
import os import os
import shutil import shutil
from anki.config import Config
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from tests.shared import getEmptyCol from tests.shared import getEmptyCol
def test_latex(): def test_latex():
col = getEmptyCol() col = getEmptyCol()
col.set_config_bool(Config.Bool.RENDER_LATEX, True)
# change latex cmd to simulate broken build # change latex cmd to simulate broken build
import anki.latex import anki.latex
@ -51,49 +53,9 @@ def test_latex():
assert ".png" in oldcard.question() assert ".png" in oldcard.question()
# if we turn off building, then previous cards should work, but cards with # if we turn off building, then previous cards should work, but cards with
# missing media will show a broken image # missing media will show a broken image
anki.latex.build = False col.set_config_bool(Config.Bool.RENDER_LATEX, False)
note = col.newNote() note = col.newNote()
note["Front"] = "[latex]foo[/latex]" note["Front"] = "[latex]foo[/latex]"
col.addNote(note) col.addNote(note)
assert len(os.listdir(col.media.dir())) == 2 assert len(os.listdir(col.media.dir())) == 2
assert ".png" in oldcard.question() assert ".png" in oldcard.question()
# turn it on again so other test don't suffer
anki.latex.build = True
# bad commands
(result, msg) = _test_includes_bad_command("\\write18")
assert result, msg
(result, msg) = _test_includes_bad_command("\\readline")
assert result, msg
(result, msg) = _test_includes_bad_command("\\input")
assert result, msg
(result, msg) = _test_includes_bad_command("\\include")
assert result, msg
(result, msg) = _test_includes_bad_command("\\catcode")
assert result, msg
(result, msg) = _test_includes_bad_command("\\openout")
assert result, msg
(result, msg) = _test_includes_bad_command("\\write")
assert result, msg
(result, msg) = _test_includes_bad_command("\\loop")
assert result, msg
(result, msg) = _test_includes_bad_command("\\def")
assert result, msg
(result, msg) = _test_includes_bad_command("\\shipout")
assert result, msg
# inserting commands beginning with a bad name should not raise an error
(result, msg) = _test_includes_bad_command("\\defeq")
assert not result, msg
# normal commands should not either
(result, msg) = _test_includes_bad_command("\\emph")
assert not result, msg
def _test_includes_bad_command(bad):
col = getEmptyCol()
note = col.newNote()
note["Front"] = f"[latex]{bad}[/latex]"
col.addNote(note)
q = without_unicode_isolation(note.cards()[0].question())
return (f"'{bad}' is not allowed on cards" in q, f"Card content: {q}")

View File

@ -6,7 +6,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>606</width> <width>636</width>
<height>638</height> <height>638</height>
</rect> </rect>
</property> </property>
@ -347,7 +347,7 @@
<property name="title"> <property name="title">
<string>preferences_review</string> <string>preferences_review</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_16"> <layout class="QVBoxLayout" name="verticalLayout_5">
<item> <item>
<widget class="QCheckBox" name="showPlayButtons"> <widget class="QCheckBox" name="showPlayButtons">
<property name="sizePolicy"> <property name="sizePolicy">
@ -413,6 +413,19 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="render_latex">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>preferences_generate_latex_images_automatically</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@ -1098,6 +1111,7 @@
<tabstop>showProgress</tabstop> <tabstop>showProgress</tabstop>
<tabstop>showEstimates</tabstop> <tabstop>showEstimates</tabstop>
<tabstop>spacebar_rates_card</tabstop> <tabstop>spacebar_rates_card</tabstop>
<tabstop>render_latex</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

@ -126,6 +126,7 @@ class Preferences(QDialog):
form.paste_strips_formatting.setChecked(editing.paste_strips_formatting) form.paste_strips_formatting.setChecked(editing.paste_strips_formatting)
form.ignore_accents_in_search.setChecked(editing.ignore_accents_in_search) form.ignore_accents_in_search.setChecked(editing.ignore_accents_in_search)
form.pastePNG.setChecked(editing.paste_images_as_png) form.pastePNG.setChecked(editing.paste_images_as_png)
form.render_latex.setChecked(editing.render_latex)
form.default_search_text.setText(editing.default_search_text) form.default_search_text.setText(editing.default_search_text)
form.backup_explanation.setText( form.backup_explanation.setText(
@ -154,6 +155,7 @@ class Preferences(QDialog):
editing.adding_defaults_to_current_deck = not form.useCurrent.currentIndex() editing.adding_defaults_to_current_deck = not form.useCurrent.currentIndex()
editing.paste_images_as_png = self.form.pastePNG.isChecked() editing.paste_images_as_png = self.form.pastePNG.isChecked()
editing.paste_strips_formatting = self.form.paste_strips_formatting.isChecked() editing.paste_strips_formatting = self.form.paste_strips_formatting.isChecked()
editing.render_latex = self.form.render_latex.isChecked()
editing.default_search_text = self.form.default_search_text.text() editing.default_search_text = self.form.default_search_text.text()
editing.ignore_accents_in_search = ( editing.ignore_accents_in_search = (
self.form.ignore_accents_in_search.isChecked() self.form.ignore_accents_in_search.isChecked()

View File

@ -36,6 +36,7 @@ impl From<BoolKeyProto> for BoolKey {
BoolKeyProto::ResetCountsReviewer => BoolKey::ResetCountsReviewer, BoolKeyProto::ResetCountsReviewer => BoolKey::ResetCountsReviewer,
BoolKeyProto::RandomOrderReposition => BoolKey::RandomOrderReposition, BoolKeyProto::RandomOrderReposition => BoolKey::RandomOrderReposition,
BoolKeyProto::ShiftPositionOfExistingCards => BoolKey::ShiftPositionOfExistingCards, BoolKeyProto::ShiftPositionOfExistingCards => BoolKey::ShiftPositionOfExistingCards,
BoolKeyProto::RenderLatex => BoolKey::RenderLatex,
} }
} }
} }

View File

@ -27,6 +27,7 @@ pub enum BoolKey {
NewCardsIgnoreReviewLimit, NewCardsIgnoreReviewLimit,
PasteImagesAsPng, PasteImagesAsPng,
PasteStripsFormatting, PasteStripsFormatting,
RenderLatex,
PreviewBothSides, PreviewBothSides,
RestorePositionBrowser, RestorePositionBrowser,
RestorePositionReviewer, RestorePositionReviewer,

View File

@ -128,6 +128,7 @@ impl Collection {
paste_strips_formatting: self.get_config_bool(BoolKey::PasteStripsFormatting), paste_strips_formatting: self.get_config_bool(BoolKey::PasteStripsFormatting),
default_search_text: self.get_config_string(StringKey::DefaultSearchText), default_search_text: self.get_config_string(StringKey::DefaultSearchText),
ignore_accents_in_search: self.get_config_bool(BoolKey::IgnoreAccentsInSearch), ignore_accents_in_search: self.get_config_bool(BoolKey::IgnoreAccentsInSearch),
render_latex: self.get_config_bool(BoolKey::RenderLatex),
}) })
} }
@ -141,6 +142,7 @@ impl Collection {
self.set_config_bool_inner(BoolKey::PasteStripsFormatting, s.paste_strips_formatting)?; self.set_config_bool_inner(BoolKey::PasteStripsFormatting, s.paste_strips_formatting)?;
self.set_config_string_inner(StringKey::DefaultSearchText, &s.default_search_text)?; self.set_config_string_inner(StringKey::DefaultSearchText, &s.default_search_text)?;
self.set_config_bool_inner(BoolKey::IgnoreAccentsInSearch, s.ignore_accents_in_search)?; self.set_config_bool_inner(BoolKey::IgnoreAccentsInSearch, s.ignore_accents_in_search)?;
self.set_config_bool_inner(BoolKey::RenderLatex, s.render_latex)?;
Ok(()) Ok(())
} }
} }