STACK: Update code for latest STACK API version and add feature testsuite example - #3043
STACK: Update code for latest STACK API version and add feature testsuite example#3043geoo89 wants to merge 13 commits into
Conversation
|
@geoo89 - thanks for the recent PRs for STACK. Pretty much every contributor's contributions is now getting a first pass from Claude Pro. It is not just a totally automated thing, I use an initial review for a first look and what gets posted here is adjusted based on that. So not a full review, but not a complete robot either. Coming next. |
|
Thanks for this — it's a substantial, carefully-explained piece of work, and the commit messages make the reasoning easy to follow. The security-relevant changes (dropping 1. Don't include the 2. Please keep 3. 4. 5. Error message. The 6. Example file header. None of these are structural — the design is sound and the bug/security fixes are welcome. Mostly it's dropping the dist commit, reverting the reformat, and reusing the existing id template. Claude Opus 4.8, acting as a review assistant for Rob Beezer |
|
A force-push is always fine at junctures like this one - necessary changes thoughout a list of commits. |
a4a17b0 to
a9051f2
Compare
|
I've amended some of the commits to address the issues above. |
|
@geoo89 — a follow-up on point 6 (the file header), with an apology attached. The intent of that point was to replace the old (You'll notice some older files still say "MathBook XML" — that's a cleanup pending on our side, not the form to copy.) And a genuine apology for the delay. When @rbeezer re-reviewed the force-pushed commits, the removed header was in fact caught and written up as a blocker — but that note was never actually posted, so it's only reaching you now, a week on. That lapse is entirely on us; sorry to have left it hanging, and thanks for your patience. Claude Opus 4.8 (1M context), acting as a review assistant for Rob Beezer |
a9051f2 to
c2face0
Compare
|
Thanks, I've added the new header and amended the first commit. |
|
Re-reviewed from scratch at the current head. All six earlier requests are properly addressed — no Remaining items, mostly small:
One non-blocking observation, since you're well placed to carry it upstream: Claude Fable 5, acting as a review assistant for Rob Beezer |
vle_reset_question_registry() clears every iframe belonging to a question boundary, which is only safe when the whole question body is about to be re-rendered (send()). validate() and answer() only patch a single validation span or the score/feedback elements, so calling it there was orphaning any other live iframe in the same question (e.g. a Parsons/drag-and-drop/JSXgraph widget) from the postMessage system, since its registry entries were deleted without the iframe actually being recreated.
Adds a STACK question's id attribute to the <div> HTML output element. createQuestionBlocks() generated qprefix as "q" + positional index, discarding the id the XSL already assigns to every "que stack" div (from @label/@xml:id, required at build time). Using the existing id keeps the prefix stable and traceable to the source exercise instead of depending on page order.
create_iframe() added allow-same-origin to the sandbox for "non-evil"
iframes to (supposedly) make <base href> resolve relative URLs. Since
these are srcdoc iframes, allow-same-origin instead makes them
same-origin with the embedding textbook page itself (not the STACK
API), and combined with allow-scripts that gives iframe content full
read/write access to our DOM, cookies, etc -- a complete sandbox
escape.
The Moodle source this file was ported from explicitly warns against
this on the same line ("UNDER NO CIRCUMSTANCES DO WE ALLOW-SAME-ORIGIN!
That would defeat the whole point of this.") and never included it.
The <base href> injection this was meant to support already existed
and worked before allow-same-origin was added, so it was unnecessary.
The STACK API restarts its iframe-id numbering on every render/
validate/grade call, so two different STACK questions on the same
page can easily get iframes with the identical API-assigned id (e.g.
both "0"), and the served iframe content self-reports that same id in
its postMessage traffic regardless of what DOM id we give it.
The old code used that raw id directly as the key into the global
IFRAMES/IFRAME_TO_BOUNDARY registries, so activating a second question
silently overwrote the first question's registry entry. Going back to
the first question and interacting with its (still live) iframe then
routed its messages to the second question's boundary instead -- e.g.
dragging pieces in one Parsons question after visiting another would
write the answer into the wrong question's hidden input, leaving the
original question empty at submit time ("Please enter valid answers
for all parts of the question").
Fix: identify the sender of each postMessage via e.source (the actual
window that sent it, which the browser guarantees is unique) instead
of the API-supplied msg.src. create_iframe() now mints its own
collision-proof key for internal bookkeeping, keeps the API's raw id
around separately (IFRAME_RAW_ID) since the iframe's own script still
needs to see it echoed back as "tgt", and a WeakMap resolves incoming
messages' e.source back to that key.
The STACK API accepts a question definition as either a Moodle XML quiz bank or a single-question yaml file, but stackapicalls.js only ever tried to DOMParser-parse the fetched qfile as XML -- a .yaml source would fail outright. getQuestionFile() now dispatches on the qfile extension. XML keeps the existing DOM-based extraction (loadQuestionFromXml, renamed from loadQuestionFromFile). For yaml, a STACK yaml file holds exactly one question and the API accepts its raw contents as questionDefinition directly (confirmed against the live API) -- so loadQuestionFromYaml only needs to pull a random variant out of the file's own deployedseed list (block or flow style), matching the random-variant selection already done for XML's "show new example question". Also tightens collectData()'s "did we get a question" check from a string-sentinel comparison to a straightforward truthiness check on the new, format-agnostic questionDefinition field, which incidentally fixes the pre-existing empty-qfile (managed-directories) case sending a request body with questionDefinition: undefined.
…m one Dropdown (and other permutation-dependent) inputs derive their option order from random_permutation(), which is seeded per-request. validate() called collectData() without fixing the seed, so it silently re-picked a new random deployed seed on every keystroke, reinterpreting the submitted option index against a different permutation and showing the wrong option/answer note in the validation feedback. answer() already guarded against this by overwriting data.seed with the stored render seed; validate() now does the same.
The STACK API now proxies plots/files through plot.php instead of linking directly into the plots directory, so a submitted file can't be accessed/run directly. Request the new path by default and fall back to the old direct link if it 404s, since older API deployments don't have plot.php yet. This affects the JS client and static version generation in _stack_download_assets.
Previously only a PDF was kept, so an EPUB (which can't display PDF) fell back to a text placeholder for any STACK exercise with a plot. Save the SVG as received and derive both a PDF (LaTeX/print) and a PNG (Kindle) from it, so every output route gets its preferred format with no fallback needed. Fixes PreTeXtBook#3033.
The catch block wrote to document.getElementById('errors'), which
doesn't exist -- each question's error div is namespaced as
qprefix+'errors', so the handler itself threw instead of surfacing
the download error. qprefix is already a parameter here.
send()'s id/for rewrite prefixes ids but not radio input names, and radios sharing a name with no form owner form one document-wide group. Two questions both using an input called "ans1" (e.g. Radio and Radio_compact in the testsuite) rendered name="stackapi_input_ans1" on the same page, so selecting one deselected the other and it graded as unanswered. Wrapping each question's shell in its own <form> scopes radio-button grouping to that form, without needing to touch collectAnswer or the vle name lookups. onsubmit="return false" keeps Enter-key implicit submission from reloading the page.
The Moodle original restricted change-content/get-content/toggle- visibility lookups to the question's own container. This port's vle_get_element() looked up any id on the page, so a question's iframe could read or replace content belonging to a different question (or outside any question), crossing the per-question isolation the rest of this file establishes. Every call site already knows the sender's key via the postMessage handler, so scope the lookup to IFRAME_TO_BOUNDARY[key].
c2face0 to
9546958
Compare
|
I've addressed the comments above. 2, 3, and 4 are separate commits for transparency's sake while the other changes got squashed into the first commit that creates the article. I left the publication base URL placeholder as there is no live deployment to point to. |
|
|
Forgot to say: the reworked branch has been merged - so all new hashes, etc, but no real code changes |
|
When you built teh sample article with It is a weird situation. admittedly, but it needs to be addressed - the sample article should never be incorrect. |
The official STACK API deployment was updated on 2026-07-16 with a security update: The API was retrieving plots and files directly from the server, and theoretically a user could submit a question containing a dodgy file and then access and run it. The update serves plots and files indirectly as STACK does normally, so the URL of these assets has changed.
This PR updates the JS client and static version generation for STACK questions to address this.
It also fixes a few more bugs in the JS version that we've encountered over time, and supports a few more STACK features. This is tested against a new example showcasing a variety of STACK questions (mostly test cases shipping with the official STACK library).
It appears that now the JS shipped with compiled HTML versions comes from
js/dist, so there is one commit copying the effective changes to the STACK JS files into that folder.Along the way we're now also generating extracted images in SVG and PNG formats. This fixes #3033.
Joint work with my colleague @murungadaniel and Claude.