Files
Classroom_Desktop/tools/lab/pdf-check
T
ekklesiaandClaude Opus 5 5f2a9098c1 fix: a worksheet a lesson downloads comes back as a tab
Some Wonder Ink lesson links are a download rather than a page: the
server sends the PDF as an attachment, so there is nothing for a tab to
show and the file lands on disk.

In this room that is the same as it not arriving. There is no file
manager, no application launcher and no downloads bar, on purpose, and a
teacher is not going to open a maintenance terminal to print a Christmas
tree. 0.2.1 downloaded the worksheet, said so in a notification, and left
it somewhere unreachable. Found on the canary machine with a real lesson.

So a download that completes, and really is a PDF, opens as another tab.
Rendering is the engine's own viewer — the same PDFium Chrome uses — so
there is no second rendering library and no separate application. That
was verified against a completed local file rather than assumed from the
remote case: PdfViewerEnabled was already on, and a local file: URL is a
different question.

WHAT IS REFUSED. A completed download is not a usable worksheet. HTML
that a server called .pdf, an empty file, an unreadable one: all refused
on the first five bytes rather than on the name, because the alternative
is a blank tab that says nothing. An interrupted download says it did not
finish and to try again — and that needed stateChanged rather than
isFinishedChanged, which is not emitted for an interrupted download at
all. Connected to the obvious signal, this said nothing whatsoever about
a worksheet that failed to arrive, which is the one case a teacher most
needs told about. Measured.

WHICH LOCAL FILES. Exactly the ones the window opened itself. Opening a
worksheet adds one resolved path to a list, and the page's
acceptNavigationRequest refuses every file: URL that is not on it.
Chromium already refuses to let a remote page reach file: — verified with
a link, a location.href, and a link to a worksheet that had been
downloaded but not opened by the shell, all refused — so this is the
second lock rather than the only one. It is here because the first one is
somebody else's. Two Qt defaults are turned off beside it:
LocalContentCanAccessFileUrls, which is on by default and would let a
local document read other local documents, and its remote counterpart.
Print to File is removed from the print dialog, because choosing it
enables a path with a "..." button, which is a file chooser, in a room
whose whole design is that there is no file chooser. The PDF viewer's own
menus were audited: two-page view, annotations, present, properties, and
nothing that opens or saves anywhere a teacher chooses.

AND THE BROWSER IS NOT INVOLVED. The handler is connected to Wonder Ink's
own profile, so it only ever sees downloads that started in a Wonder Ink
tab. Nothing inspects a filename, nothing watches a directory, and Chrome
— a separate application with a separate profile — is untouched:
classroom-browser and the Chrome policies are byte-identical to stable
0.2.0. Worksheets go to ~/Downloads/Wonder Ink, which is tidiness and
mostly the security argument: it is the only place one can come from.

Only PDFs. Anything else downloads, says so, and is still unreachable;
that limitation and two small follow-ups are written down rather than
built, and nothing downloaded is ever executed.

Measured from the package on a rebooted machine, and again on one
upgraded from the published 0.2.1: the click-to-tab workflow end to end,
a name with spaces and brackets, multi-page navigation, printing to
Elmer, closing the worksheet without closing the lesson, and the four
refusals. 120 convergence checks and 24 worksheet checks pass on both.
Cookies are still session-only, the Present-view autoplay fix still
works, fullscreen still comes back maximised, and there is still no
address bar and no new-tab button.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-19 22:40:16 -05:00

140 lines
5.5 KiB
Bash
Executable File

#!/bin/bash
# Does Wonder Ink open the worksheets it should and refuse the files it
# should not?
#
# The download-to-tab workflow is driven by hand against a running
# session; this is the part of it that can be asserted rather than looked
# at — which local files the shell will load, and which it will not.
#
# It runs inside the lab, because the classroom's own PyQt6 is there and
# the development host has no Qt at all. It imports the *installed*
# command, so it is testing what the package put on the machine.
set -uo pipefail
NAME="${LAB_NAME:-classroom-v2-lab}"
docker exec -i \
-e QT_QPA_PLATFORM=offscreen \
-e QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu" \
-e XDG_RUNTIME_DIR=/tmp/pdfcheck-rt \
-e HOME=/root \
"$NAME" bash -c 'mkdir -p /tmp/pdfcheck-rt && chmod 700 /tmp/pdfcheck-rt && python3 -' <<'PY'
import importlib.util, sys, tempfile
from importlib.machinery import SourceFileLoader
from pathlib import Path
loader = SourceFileLoader("wonderink", "/usr/local/bin/classroom-wonderink")
spec = importlib.util.spec_from_loader("wonderink", loader)
ink = importlib.util.module_from_spec(spec)
loader.exec_module(ink)
from PyQt6.QtCore import QUrl
from PyQt6.QtWidgets import QApplication
from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEnginePage
app = QApplication(sys.argv[:1])
profile = QWebEngineProfile("pdfcheck", app)
window = ink.WonderInk(profile)
passed = failed = 0
def check(label, condition, detail=""):
global passed, failed
if condition:
print(f" \033[32mPASS\033[0m {label}")
passed += 1
else:
print(f" \033[31mFAIL\033[0m {label}" + (f" [{detail}]" if detail else ""))
failed += 1
work = Path(tempfile.mkdtemp())
real = work / "worksheet.pdf"
real.write_bytes(b"%PDF-1.4\n% a worksheet\n")
spaces = work / "Leader Guide, Week 1 (Advent).pdf"
spaces.write_bytes(b"%PDF-1.4\n% another\n")
htmlish = work / "broken.pdf"
htmlish.write_bytes(b"<html><body>404</body></html>")
empty = work / "empty.pdf"
empty.write_bytes(b"")
notes = work / "notes.txt"
notes.write_text("bring scissors")
print("1. which local files the window will open")
check("nothing at all, before anything is downloaded",
not window.may_open(Path("/etc/passwd")))
check("not a file that merely exists", not window.may_open(real))
page = ink.LessonPage(profile, window)
check("and a page may not navigate to one",
page.acceptNavigationRequest(
QUrl.fromLocalFile("/etc/passwd"),
QWebEnginePage.NavigationType.NavigationTypeLinkClicked, True) is False)
check("nor to a worksheet that was never opened",
page.acceptNavigationRequest(
QUrl.fromLocalFile(str(real)),
QWebEnginePage.NavigationType.NavigationTypeLinkClicked, True) is False)
print()
print("2. a worksheet the window itself opened")
window.open_pdf(real)
check("is openable afterwards", window.may_open(real))
check("and only it", not window.may_open(Path("/etc/passwd")))
check("a page may navigate to that one",
page.acceptNavigationRequest(
QUrl.fromLocalFile(str(real)),
QWebEnginePage.NavigationType.NavigationTypeLinkClicked, True) is True)
check("a symlink to somewhere else is not laundered into permission",
not window.may_open(Path("/etc/passwd")))
print()
print("3. which schemes are allowed at all")
for scheme, want in (("https://app.wonderink.org/", True),
("http://example.org/", True),
("about:blank", True),
("mailto:[email protected]", False),
("ftp://example.org/x", False),
("javascript:alert(1)", False)):
check(f"{scheme} -> {want}",
page.acceptNavigationRequest(
QUrl(scheme),
QWebEnginePage.NavigationType.NavigationTypeLinkClicked, True) is want)
print()
print("4. which downloads become a worksheet tab")
before = window._tabs.count()
check("a real PDF does", window.open_worksheet(spaces, "application/pdf") is True)
check("and it made a tab", window._tabs.count() == before + 1)
check("a PDF by extension alone does", window.open_worksheet(spaces) is True)
before = window._tabs.count()
check("HTML called .pdf does not open a blank tab",
window.open_worksheet(htmlish, "application/pdf") is True
and window._tabs.count() == before)
check("an empty file does not either",
window.open_worksheet(empty, "application/pdf") is True
and window._tabs.count() == before)
check("a file that is not there at all is handled",
window.open_worksheet(work / "missing.pdf", "application/pdf") is True
and window._tabs.count() == before)
check("plain text is not a worksheet and is left to the notification",
window.open_worksheet(notes, "text/plain") is False
and window._tabs.count() == before)
check("and a text file named .pdf is refused on its contents",
window.open_worksheet(htmlish) is True and window._tabs.count() == before)
print()
print("5. names with spaces and punctuation survive the round trip")
window.open_pdf(spaces)
url = QUrl.fromLocalFile(str(spaces.resolve()))
# toString() is the display form and shows the spaces as spaces; the
# encoded form is what actually goes on the wire.
check("the URL percent-encodes rather than breaks",
"%20" in bytes(url.toEncoded()).decode()
and url.toLocalFile() == str(spaces.resolve()))
check("and the window will open exactly that path", window.may_open(spaces))
print()
print(f"{passed} passed, {failed} failed")
sys.exit(1 if failed else 0)
PY