fix(task_execute): lotto/seriale in |tojson invece di letterale JS (#32)

lotNumber/serialNumber erano '{{ lot_number or '' }}': autoescape HTML dentro
uno <script>, non escaping JS/JSON. '&' diventava '&amp;' e finiva salvato nel
DB a ogni misura e a production/start; un lotto con backslash rompeva la
sintassi JS della pagina. |tojson produce un letterale JSON valido sia come JS
che come HTML, come già fa refuseWhileBlocked() più sotto nello stesso file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:40:26 +02:00
parent d40c74870e
commit c8caf40af9
2 changed files with 91 additions and 2 deletions
@@ -926,8 +926,8 @@ function taskExecute() {
// ---- Data from server ---- // ---- Data from server ----
task: window.__taskData, task: window.__taskData,
subtasks: window.__taskSubtasks, subtasks: window.__taskSubtasks,
lotNumber: '{{ lot_number or '' }}', lotNumber: {{ (lot_number or '')|tojson }},
serialNumber: '{{ serial_number or '' }}', serialNumber: {{ (serial_number or '')|tojson }},
// ---- Measurement state ---- // ---- Measurement state ----
currentIndex: 0, currentIndex: 0,
measurements: [], // [{subtask_id, value, pass_fail, deviation}, ...] measurements: [], // [{subtask_id, value, pass_fail, deviation}, ...]
@@ -0,0 +1,89 @@
"""gitea:Adriano/TieMeasureFlow#32.
lotNumber/serialNumber used to be interpolated into task_execute.html's
<script> block as a quoted JS string literal (``'{{ lot_number or '' }}'``),
which relies on Jinja's *HTML* autoescaping - the wrong escaping for a value
that has to be valid *inside a <script> block*. A lot number containing ``&``
came back HTML-entity-escaped (``A&amp;B-12``) instead of the literal
character, silently corrupting the value sent to the server on every
measurement and on production/start. A lot ending in a backslash produced
invalid JavaScript outright.
``|tojson`` is the fix: it produces a JSON literal, safe both as JS source
and as HTML, matching how every other user-supplied value on this page is
already embedded (see ``refuseWhileBlocked()``'s comment on the same file).
"""
from tests.conftest import api_get_router
TASK = {
"id": 11,
"recipe_id": 3,
"version_id": 5,
"task_type": "measure",
"title": "Quota",
"directive": None,
"description": None,
"file_path": None,
"file_type": None,
"annotations_json": None,
"subtasks": [{
"id": 21, "marker_number": 1, "description": "Altezza", "order_index": 0,
"nominal": 10.0, "utl": 10.5, "uwl": 10.2, "lwl": 9.8, "ltl": 9.5,
"unit": "mm",
}],
}
RECIPE = {
"id": 3, "code": "REC-1", "name": "Ricetta",
"measurement_interval_minutes": 30, "allow_manual_input": True,
"current_version": {"id": 5, "version_number": 1},
}
def _execute_page(client, mock_api_client, monkeypatch, lot_number, serial_number):
with client.session_transaction() as sess:
sess["language"] = "it"
sess["lot_number"] = lot_number
sess["serial_number"] = serial_number
# task_execute() only renders a recipe assigned to this station (#24);
# not what #32 is about, but the route 403s without it.
monkeypatch.setattr("blueprints.measure.Config.STATION_CODE", "ST-TEST")
mock_api_client.get_station_recipes.return_value = {"items": [{"id": 3}]}
mock_api_client.get.side_effect = api_get_router({
"/api/tasks/11": TASK,
"/api/recipes/3/tasks": [{"id": 11, "order_index": 0}],
"/api/recipes/3": RECIPE,
})
resp = client.get("/measure/execute/11")
assert resp.status_code == 200
return resp.get_data(as_text=True)
def test_ampersand_in_lot_number_is_not_html_escaped(logged_in_client, mock_api_client, monkeypatch):
html = _execute_page(logged_in_client, mock_api_client, monkeypatch, "A&B-12", "S1")
# The old bug, scoped to the <script> data block: the JS literal for
# lotNumber must not carry HTML-entity escaping (that is correct for the
# plain-text display of the same value elsewhere on the page, but breaks
# the value actually sent to the server on every measurement).
assert 'lotNumber: "A&amp;B-12"' not in html
# tojson's escaping for "&" is the \uXXXX form.
assert 'lotNumber: "A\\u0026B-12"' in html
def test_backslash_in_serial_number_stays_valid_json(logged_in_client, mock_api_client, monkeypatch):
# The old literal '{{ serial_number or '' }}' would end with a bare
# backslash escaping the closing quote - a SyntaxError, not merely a
# wrong value. tojson escapes it instead.
html = _execute_page(logged_in_client, mock_api_client, monkeypatch, "L1", "LOT\\")
assert 'serialNumber: "LOT\\\\"' in html
def test_apostrophe_in_lot_number_stays_a_single_json_string(logged_in_client, mock_api_client, monkeypatch):
# The old literal '{{ lot_number or '' }}' would have closed its own
# quoted string early on the apostrophe. tojson escapes it to ',
# which the JS engine reads back as the same original character.
html = _execute_page(logged_in_client, mock_api_client, monkeypatch, "O'Brien-7", "S2")
assert 'lotNumber: "O\\u0027Brien-7"' in html