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-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

View File

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

View File

@ -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:

View File

@ -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}")

View File

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

View File

@ -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()

View File

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

View File

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

View File

@ -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(())
}
}