From 06f7aa393d21d7d5dd8039e15d543b73c3346932 Mon Sep 17 00:00:00 2001 From: Abdo Date: Sat, 1 Jun 2024 11:26:28 +0300 Subject: [PATCH] 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) --- ftl/core/preferences.ftl | 2 ++ proto/anki/config.proto | 2 ++ pylib/anki/latex.py | 29 +++++------------------- pylib/tests/test_latex.py | 44 +++---------------------------------- qt/aqt/forms/preferences.ui | 18 +++++++++++++-- qt/aqt/preferences.py | 2 ++ rslib/src/backend/config.rs | 1 + rslib/src/config/bool.rs | 1 + rslib/src/preferences.rs | 2 ++ 9 files changed, 35 insertions(+), 66 deletions(-) diff --git a/ftl/core/preferences.ftl b/ftl/core/preferences.ftl index 04243a494..3e0a2414b 100644 --- a/ftl/core/preferences.ftl +++ b/ftl/core/preferences.ftl @@ -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-paste-clipboard-images-as-png = Paste clipboard images as PNG 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-please-restart-anki-to-complete-language = Please restart Anki to complete language change. preferences-preferences = Preferences diff --git a/proto/anki/config.proto b/proto/anki/config.proto index 1eda78e03..2cee6172b 100644 --- a/proto/anki/config.proto +++ b/proto/anki/config.proto @@ -53,6 +53,7 @@ message ConfigKey { RESET_COUNTS_REVIEWER = 22; RANDOM_ORDER_REPOSITION = 23; SHIFT_POSITION_OF_EXISTING_CARDS = 24; + RENDER_LATEX = 25; } enum String { SET_DUE_BROWSER = 0; @@ -121,6 +122,7 @@ message Preferences { bool paste_strips_formatting = 3; string default_search_text = 4; bool ignore_accents_in_search = 5; + bool render_latex = 6; } message BackupLimits { uint32 daily = 1; diff --git a/pylib/anki/latex.py b/pylib/anki/latex.py index e492422c8..75c7d9e9f 100644 --- a/pylib/anki/latex.py +++ b/pylib/anki/latex.py @@ -5,12 +5,12 @@ from __future__ import annotations import html import os -import re from dataclasses import dataclass import anki import anki.collection from anki import card_rendering_pb2, hooks +from anki.config import Config from anki.models import NotetypeDict from anki.template import TemplateRenderContext, TemplateRenderOutput 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"], ] -# if off, use existing media but don't create new -build = True # pylint: disable=invalid-name - # add standard tex install location to osx if is_mac: os.environ["PATH"] += ":/usr/texbin:/Library/TeX/texbin" @@ -104,11 +101,15 @@ def render_latex_returning_errors( out = ExtractedLatexOutput.from_proto(proto) errors = [] html = out.html + render_latex = col.get_config_bool(Config.Bool.RENDER_LATEX) for latex in out.latex: # don't need to render? - if not build or col.media.have(latex.filename): + if col.media.have(latex.filename): 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) if err is not None: @@ -126,24 +127,6 @@ def _save_latex_image( ) -> str | None: # add header/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 if svg: diff --git a/pylib/tests/test_latex.py b/pylib/tests/test_latex.py index 89c8229a3..41dc14bf6 100644 --- a/pylib/tests/test_latex.py +++ b/pylib/tests/test_latex.py @@ -6,12 +6,14 @@ import os import shutil +from anki.config import Config from anki.lang import without_unicode_isolation from tests.shared import getEmptyCol def test_latex(): col = getEmptyCol() + col.set_config_bool(Config.Bool.RENDER_LATEX, True) # change latex cmd to simulate broken build import anki.latex @@ -51,49 +53,9 @@ def test_latex(): assert ".png" in oldcard.question() # if we turn off building, then previous cards should work, but cards with # missing media will show a broken image - anki.latex.build = False + col.set_config_bool(Config.Bool.RENDER_LATEX, False) note = col.newNote() note["Front"] = "[latex]foo[/latex]" col.addNote(note) assert len(os.listdir(col.media.dir())) == 2 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}") diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui index 8e5572783..871c07f9b 100644 --- a/qt/aqt/forms/preferences.ui +++ b/qt/aqt/forms/preferences.ui @@ -6,7 +6,7 @@ 0 0 - 606 + 636 638 @@ -347,7 +347,7 @@ preferences_review - + @@ -413,6 +413,19 @@ + + + + + 0 + 0 + + + + preferences_generate_latex_images_automatically + + + @@ -1098,6 +1111,7 @@ showProgress showEstimates spacebar_rates_card + render_latex pastePNG paste_strips_formatting useCurrent diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index 89e4ac8e0..b0af7cce2 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -126,6 +126,7 @@ class Preferences(QDialog): form.paste_strips_formatting.setChecked(editing.paste_strips_formatting) form.ignore_accents_in_search.setChecked(editing.ignore_accents_in_search) 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.backup_explanation.setText( @@ -154,6 +155,7 @@ class Preferences(QDialog): editing.adding_defaults_to_current_deck = not form.useCurrent.currentIndex() editing.paste_images_as_png = self.form.pastePNG.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.ignore_accents_in_search = ( self.form.ignore_accents_in_search.isChecked() diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs index 7637d30d1..721d84fc9 100644 --- a/rslib/src/backend/config.rs +++ b/rslib/src/backend/config.rs @@ -36,6 +36,7 @@ impl From for BoolKey { BoolKeyProto::ResetCountsReviewer => BoolKey::ResetCountsReviewer, BoolKeyProto::RandomOrderReposition => BoolKey::RandomOrderReposition, BoolKeyProto::ShiftPositionOfExistingCards => BoolKey::ShiftPositionOfExistingCards, + BoolKeyProto::RenderLatex => BoolKey::RenderLatex, } } } diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs index 1177baa71..4f320cd76 100644 --- a/rslib/src/config/bool.rs +++ b/rslib/src/config/bool.rs @@ -27,6 +27,7 @@ pub enum BoolKey { NewCardsIgnoreReviewLimit, PasteImagesAsPng, PasteStripsFormatting, + RenderLatex, PreviewBothSides, RestorePositionBrowser, RestorePositionReviewer, diff --git a/rslib/src/preferences.rs b/rslib/src/preferences.rs index 6ee564d3f..cfeae3fa3 100644 --- a/rslib/src/preferences.rs +++ b/rslib/src/preferences.rs @@ -128,6 +128,7 @@ impl Collection { paste_strips_formatting: self.get_config_bool(BoolKey::PasteStripsFormatting), default_search_text: self.get_config_string(StringKey::DefaultSearchText), 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_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::RenderLatex, s.render_latex)?; Ok(()) } }