#!/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"404") 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:someone@example.org", 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