Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
if-no-files-found: warn
- name: Test landing page rendering
shell: powershell
run: node --test .\tests\site.test.cjs
run: node --test .\tests\site.test.cjs .\tests\embed.test.cjs

required-checks:
name: Required checks
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ Pushing a stable SemVer tag starts the [Prepare release workflow](.github/workfl

Website source is in [site/](site/); the only loader source is [scans.ps1](scans.ps1). The site builder copies it unchanged into its deployment output. The actual utility is [src/scans.ps1](src/scans.ps1); release packaging selects that file, never the loader. New stable tags must use `vMAJOR.MINOR.PATCH`; historical four-part tags are retained and linked through GitHub Releases, but not exposed through the new checksum-verified channel.

The landing page follows the PrintMaster site's Solarized palette, teal accents, wordmark, panel styling, and cross-site navigation. Styles and the logo are local to this repository; navigation works without JavaScript, and the layout supports light/dark preferences, keyboard navigation, and reduced motion. Release rendering and copy buttons use [site/releases.js](site/releases.js).
The landing page follows the PrintMaster site's Solarized palette, teal accents, wordmark, and panel styling. Standalone navigation is Scans-focused: its brand links home locally, setup and release links stay on the page, and external Scans resources are explicitly labeled. The footer separates Agent/Server downloads and fleet-monitoring links under "More from PrintMaster." Styles and the logo are local to this repository; navigation works without JavaScript, and the layout supports light/dark preferences, keyboard navigation, and reduced motion. Release rendering and copy buttons use [site/releases.js](site/releases.js).

The main website can frame `https://scans.printmaster.work/?embed=1` at `/scans/`. [site/embed.js](site/embed.js) hides this site's header/footer and locks the dark palette only after a handshake from the immediate parent at `https://printmaster.work`. Without that handshake (including direct visits and JavaScript disabled), the full standalone site remains visible. Origin- and source-checked messages report content height and anchor positions to the main site's wrapper. External links open at the top level; setup commands retain their canonical standalone URLs. The main site owns its header/footer and clipboard delegation. Deploy this embed support before the main-site wrapper.

The handshake restricts embed-mode activation, not who can frame the public standalone site. GitHub Pages cannot set a custom HTTP `Content-Security-Policy: frame-ancestors` header; a meta tag cannot enforce it. If the domain later uses a configurable edge/server, set `frame-ancestors 'self' https://printmaster.work` there and avoid `X-Frame-Options: DENY`/`SAMEORIGIN`, which would block the cross-origin main-site frame.

Work on a feature branch and open a pull request. The [CI workflow](.github/workflows/ci.yml) runs syntax checks and mocked Pester tests on Windows using both Windows PowerShell 5.1 and PowerShell 7. Its **Required checks** job passes only when both test jobs succeed, including when using a merge queue. CI also runs after pushes to `main` and can be started manually.

Expand All @@ -135,7 +139,7 @@ From the repository root, run either runtime:
```powershell
powershell.exe -NoProfile -STA -File .\tests\Invoke-Tests.ps1
pwsh.exe -NoProfile -STA -File .\tests\Invoke-Tests.ps1
node --test .\tests\site.test.cjs
node --test .\tests\site.test.cjs .\tests\embed.test.cjs
```

The [test runner](tests/Invoke-Tests.ps1) fails on syntax errors, failing tests, or an empty test run. The [regression suite](tests/Scans.Tests.ps1) extracts function definitions without running the utility's elevation or setup code. Account, registry, filesystem, SMB, firewall, clipboard-related setup gating, and COM changes are mocked; WPF windows are loaded but never displayed. Tests cover password failures, error-popup routing, account visibility, folder setup, shares, network configuration, shortcuts, generated password shape, and setup options. The generator tests do not claim that every generated password satisfies policy.
Expand Down
40 changes: 40 additions & 0 deletions site/embed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
(() => {
const origin = 'https://printmaster.work';
if (window.parent === window || new URLSearchParams(location.search).get('embed') !== '1') return;
let embedded = false;
const send = data => window.parent.postMessage(data, origin);
const resize = () => send({
type: 'printmaster:scans:resize',
height: Math.ceil(document.body.getBoundingClientRect().height)
});
const navigate = hash => {
if (typeof hash !== 'string' || !/^#[a-z][a-z0-9-]*$/.test(hash)) return;
const target = document.getElementById(hash.slice(1));
if (target) send({
type: 'printmaster:scans:anchor', hash,
top: target.getBoundingClientRect().top + window.scrollY
});
};
window.addEventListener('message', event => {
if (event.origin !== origin || event.source !== window.parent ||
!event.data || event.data.type !== 'printmaster:scans:init') return;
if (!embedded) {
embedded = true;
document.documentElement.setAttribute('data-embedded', '');
document.querySelectorAll('a[href^="https://"]').forEach(link => {
link.target = '_top';
});
new ResizeObserver(resize).observe(document.body);
document.addEventListener('click', event => {
const link = event.target.closest('a[href^="#"]');
if (!link || event.defaultPrevented || event.button !== 0 ||
event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
event.preventDefault();
navigate(link.getAttribute('href'));
});
}
resize();
navigate(event.data.hash);
});
send({ type: 'printmaster:scans:ready' });
})();
20 changes: 10 additions & 10 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@
<link rel="icon" href="logo.svg" type="image/svg+xml">
<link rel="stylesheet" href="styles.css">
<script src="releases.js" defer></script>
<script src="embed.js" defer></script>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<div class="container nav-wrap">
<a class="brand" href="https://printmaster.work/" aria-label="PrintMaster home"><img src="logo.svg" alt="" width="40" height="40"><span>Print<span class="brand-accent">Master</span></span></a>
<nav aria-label="Main navigation">
<a href="https://printmaster.work/">Platform</a>
<a href="https://printmaster.work/downloads">Downloads</a>
<a href="./" aria-current="page">Scans</a>
<a href="https://docs.printmaster.work/guides/scans/">Docs <span aria-hidden="true">&nearr;</span></a>
<a class="nav-github" href="https://github.com/printmaster-org/scans">GitHub <span aria-hidden="true">&nearr;</span></a>
<a class="brand" href="./" aria-label="PrintMaster Scans home"><img src="logo.svg" alt="" width="40" height="40"><span>Print<span class="brand-accent">Master</span> Scans</span></a>
<nav aria-label="Scans navigation">
<a href="#get-started">Get started</a>
<a href="#releases">Scans releases</a>
<a href="https://docs.printmaster.work/guides/scans/">Scans guide <span aria-hidden="true">&nearr;</span></a>
<a class="nav-github" href="https://github.com/printmaster-org/scans">Scans on GitHub <span aria-hidden="true">&nearr;</span></a>
</nav>
</div>
</header>
Expand Down Expand Up @@ -105,9 +105,9 @@ <h2>If something goes wrong</h2>
<footer class="site-footer">
<div class="container">
<div class="footer-top">
<div><a class="brand" href="https://printmaster.work/"><img src="logo.svg" alt="" width="40" height="40"><span>Print<span class="brand-accent">Master</span></span></a><p>Provided by PrintMaster.<br>Made for the networks you manage.</p></div>
<nav aria-label="PrintMaster sites"><h2>Explore</h2><a href="https://printmaster.work/">Platform</a><a href="https://printmaster.work/downloads">Downloads</a><a href="./" aria-current="page">PrintMaster Scans</a><a href="https://docs.printmaster.work/">Documentation</a></nav>
<nav aria-label="Scans resources"><h2>Scans resources</h2><a href="https://docs.printmaster.work/guides/scans/">Setup guide</a><a href="https://github.com/printmaster-org/scans">GitHub</a><a href="https://github.com/printmaster-org/scans/releases">All releases</a><a href="https://github.com/printmaster-org/scans/issues">Report a problem</a></nav>
<div><a class="brand" href="./" aria-label="PrintMaster Scans home"><img src="logo.svg" alt="" width="40" height="40"><span>Print<span class="brand-accent">Master</span> Scans</span></a><p>Provided by PrintMaster.<br>Standalone scan-to-SMB setup for Windows.</p></div>
<nav aria-label="Scans resources"><h2>Scans resources</h2><a href="#get-started">Set up your PC</a><a href="#releases">Scans releases</a><a href="https://docs.printmaster.work/guides/scans/">Scans setup guide <span aria-hidden="true">&nearr;</span></a><a href="https://github.com/printmaster-org/scans/issues">Report a Scans problem <span aria-hidden="true">&nearr;</span></a></nav>
<nav aria-label="More from PrintMaster"><h2>More from PrintMaster</h2><a href="https://printmaster.work/">PrintMaster fleet monitoring <span aria-hidden="true">&nearr;</span></a><a href="https://printmaster.work/downloads">Agent &amp; Server downloads <span aria-hidden="true">&nearr;</span></a><a href="https://docs.printmaster.work/">PrintMaster documentation <span aria-hidden="true">&nearr;</span></a></nav>
</div>
<div class="footer-bottom"><span>PrintMaster Scans &middot; Standalone Windows utility</span><a href="https://github.com/printmaster-org/scans">Open source on GitHub <span aria-hidden="true">&nearr;</span></a></div>
</div>
Expand Down
4 changes: 3 additions & 1 deletion site/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ pre { padding: 1rem; background: var(--code); border: 1px solid var(--border); b
.footer-top nav a:hover { color: var(--accent); }
.footer-bottom { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 1rem; border-top: 1px solid var(--border); padding-block: 1.25rem; font-size: .75rem; }
@media (prefers-color-scheme: light) {
:root { --bg: #fdf6e3; --bg-deep: #f5eedb; --panel: #eee8d5; --border: #d3cbb8; --fg: #073642; --muted: #586e75; --accent: #006b61; --button: #007b70; --button-fg: #fff; --code: #fdf6e3; }
:root:not([data-embedded]) { --bg: #fdf6e3; --bg-deep: #f5eedb; --panel: #eee8d5; --border: #d3cbb8; --fg: #073642; --muted: #586e75; --accent: #006b61; --button: #007b70; --button-fg: #fff; --code: #fdf6e3; }
}
:root[data-embedded] { color-scheme: dark; scroll-padding-top: 0; }
[data-embedded] .site-header, [data-embedded] .site-footer, [data-embedded] .skip-link { display: none; }
@media (max-width: 960px) {
.hero-layout { gap: 2rem; }
.setup-layout { grid-template-columns: 1fr; gap: 2rem; }
Expand Down
74 changes: 74 additions & 0 deletions tests/embed.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const source = fs.readFileSync(path.join(__dirname, '..', 'site', 'embed.js'), 'utf8');

function render(search = '?embed=1', framed = true) {
const messages = [];
const listeners = {};
const attributes = {};
const external = {};
let observer;
const parent = { postMessage: (data, origin) => messages.push({ data, origin }) };
const window = { parent, scrollY: 0, addEventListener: (type, fn) => { listeners[type] = fn; } };
if (!framed) window.parent = window;
vm.runInNewContext(source, {
window, location: { search }, URLSearchParams,
ResizeObserver: class {
constructor(fn) { observer = fn; }
observe() {}
},
document: {
body: { getBoundingClientRect: () => ({ height: 1800 }) },
documentElement: { setAttribute: (key, value) => { attributes[key] = value; } },
querySelectorAll: () => [external],
getElementById: id => id === 'releases' ? { getBoundingClientRect: () => ({ top: 1200 }) } : null,
addEventListener: (type, fn) => { listeners[type] = fn; }
}
});
return { messages, listeners, attributes, external, parent, resize: () => observer() };
}

test('standalone and unrequested frames never hide navigation or initialize messaging', () => {
for (const page of [render('', true), render('?embed=1', false)]) {
assert.equal(page.messages.length, 0);
assert.equal(page.listeners.message, undefined);
assert.deepEqual(page.attributes, {});
}
});

test('embed mode requires the exact trusted parent and reports size and valid anchors', () => {
const page = render();
assert.equal(page.messages[0].origin, 'https://printmaster.work');
assert.deepEqual(page.attributes, {});
for (const event of [
{ origin: 'https://evil.example', source: page.parent },
{ origin: 'https://printmaster.work', source: {} }
]) page.listeners.message({ ...event, data: { type: 'printmaster:scans:init' } });
assert.deepEqual(page.attributes, {});
const init = hash => page.listeners.message({
origin: 'https://printmaster.work', source: page.parent,
data: { type: 'printmaster:scans:init', hash }
});
init('#releases');
assert.equal(page.attributes['data-embedded'], '');
assert.equal(page.external.target, '_top');
assert.equal(page.messages[1].data.height, 1800);
assert.equal(page.messages[2].data.top, 1200);
assert.equal(page.messages[2].data.hash, '#releases');
page.resize();
assert.equal(page.messages.at(-1).data.type, 'printmaster:scans:resize');
const count = page.messages.length;
init('#missing');
init('javascript:alert(1)');
assert.equal(page.messages.length, count + 2, 'invalid targets emit size only, never scroll');
let prevented = false;
page.listeners.click({
target: { closest: () => ({ getAttribute: () => '#releases' }) },
button: 0, preventDefault: () => { prevented = true; }
});
assert.equal(prevented, true);
assert.equal(page.messages.at(-1).data.type, 'printmaster:scans:anchor');
});
13 changes: 10 additions & 3 deletions tests/site.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ test('includes local PrintMaster branding, a home link, and the canonical Scans
const logo = fs.readFileSync(path.join(__dirname, '..', 'site', 'logo.svg'), 'utf8');
assert.match(logo, /viewBox="0 0 64 64"/);
assert.match(html, /rel="icon" href="logo\.svg"/);
assert.match(html, /href="https:\/\/printmaster\.work\/" aria-label="PrintMaster home"/);
assert.match(html, /href="\.\/" aria-label="PrintMaster Scans home"/);
assert.match(html, /<img src="logo\.svg" alt="" width="40" height="40">/);
assert.match(html, /href="https:\/\/docs\.printmaster\.work\/guides\/scans\/"/);
assert.match(html, /Provided by PrintMaster/);
Expand All @@ -26,8 +26,15 @@ test('provides cross-site navigation and working setup anchors without JavaScrip
assert.match(html, /href="https:\/\/printmaster\.work\/downloads"/);
assert.match(html, /href="https:\/\/docs\.printmaster\.work\/"/);
assert.match(html, /href="https:\/\/docs\.printmaster\.work\/guides\/manual-smb-setup\/"/);
assert.match(html, /href="\.\/" aria-current="page">Scans/);
assert.match(html, /<nav aria-label="PrintMaster sites">/);
assert.match(html, /<nav aria-label="Scans navigation">/);
assert.match(html, /<nav aria-label="More from PrintMaster">/);
assert.match(html, /href="https:\/\/printmaster\.work\/downloads">Agent &amp; Server downloads/);
assert.match(html, /href="https:\/\/printmaster\.work\/">PrintMaster fleet monitoring/);
const header = html.match(/<header[\s\S]*?<\/header>/)[0];
assert.doesNotMatch(header, /https:\/\/printmaster\.work/);
assert.match(header, /href="#releases">Scans releases/);
assert.match(header, /Scans guide <span aria-hidden="true">&nearr;/);
assert.match(header, /Scans on GitHub <span aria-hidden="true">&nearr;/);
assert.match(html, /<noscript>/);
const ids = [...html.matchAll(/\bid="([^"]+)"/g)].map(match => match[1]);
assert.equal(new Set(ids).size, ids.length, 'element IDs must be unique');
Expand Down
Loading