-
Notifications
You must be signed in to change notification settings - Fork 0
fix(a11y): expose toast updates as status messages #491
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
13efb4f
346d830
413bbd4
06293fc
84fcdd9
031b266
3643306
477c552
07cf7a1
03b4a9a
68091e9
2ca5009
df197dd
5a3607e
bb94672
aafd14c
00c475f
ff673ca
09e937f
82cef18
66d5154
700bed8
9f6e932
67fc32b
6aae407
5d85cc6
4909787
c7ea4ff
c9880d3
3560a1d
3600d22
cd7b12f
75b375a
dd50134
0befe87
20f6088
a08c828
129398c
794ecbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -741,54 +741,33 @@ function openReportModal() { | |
| export function parseMsProjectXml(xml) { | ||
| // Fully linear extract (indexOf/slice) — no dynamic RegExp and no lazy | ||
| // [\s\S]*? block collectors (those can quadratic-backtrack on truncated input). | ||
| const isXmlWhitespace = (charCode) => ( | ||
| charCode === 0x20 || charCode === 0x09 || charCode === 0x0d || charCode === 0x0a | ||
| ); | ||
| const findTagBoundary = (source, name, from, closing = false) => { | ||
| const prefix = `<${closing ? '/' : ''}${name}`; | ||
| let searchFrom = from; | ||
| for (;;) { | ||
| const start = source.indexOf(prefix, searchFrom); | ||
| if (start === -1) return null; | ||
| let delimiter = start + prefix.length; | ||
| while (delimiter < source.length && isXmlWhitespace(source.charCodeAt(delimiter))) { | ||
| delimiter += 1; | ||
| } | ||
| if (source.charCodeAt(delimiter) === 0x3e) { | ||
| return { start, end: delimiter + 1 }; | ||
| } | ||
| // Reject attributes, longer names, and non-XML whitespace while advancing | ||
| // past every inspected byte so malformed candidates are never rescanned. | ||
| searchFrom = Math.max(delimiter + 1, start + prefix.length); | ||
| } | ||
| }; | ||
| const tag = (block, name) => { | ||
| const opening = findTagBoundary(block, name, 0); | ||
| if (!opening) return ''; | ||
| const closing = findTagBoundary(block, name, opening.end, true); | ||
| const nextOpening = findTagBoundary(block, name, opening.end); | ||
| if (!closing || (nextOpening && nextOpening.start < closing.start)) return ''; | ||
| return block.slice(opening.end, closing.start).trim(); | ||
| const openingTag = `<${name}>`; | ||
| const closingTag = `</${name}>`; | ||
| const valueStart = block.indexOf(openingTag); | ||
| if (valueStart === -1) return ''; | ||
| const contentStart = valueStart + openingTag.length; | ||
| const valueEnd = block.indexOf(closingTag, contentStart); | ||
| return valueEnd === -1 ? '' : block.slice(contentStart, valueEnd).trim(); | ||
| }; | ||
| const collectBlocks = (source, name) => { | ||
| const collectBlocks = (source, openTag, closeTag) => { | ||
| const out = []; | ||
| let from = 0; | ||
| for (;;) { | ||
| const opening = findTagBoundary(source, name, from); | ||
| if (!opening) break; | ||
| const closing = findTagBoundary(source, name, opening.end, true); | ||
| const nextOpening = findTagBoundary(source, name, opening.end); | ||
| // Incomplete or nested same-name block: stop at the first unmatched | ||
| // opening tag instead of pairing it with a later block's closing tag. | ||
| if (!closing || (nextOpening && nextOpening.start < closing.start)) break; | ||
| out.push(source.slice(opening.start, closing.end)); | ||
| from = closing.end; | ||
| const start = source.indexOf(openTag, from); | ||
| if (start === -1) break; | ||
| const contentStart = start + openTag.length; | ||
| const end = source.indexOf(closeTag, contentStart); | ||
| // Incomplete open tag: stop linearly (do not rescan the remainder). | ||
| if (end === -1) break; | ||
| out.push(source.slice(start, end + closeTag.length)); | ||
| from = end + closeTag.length; | ||
| } | ||
| return out; | ||
| }; | ||
| const predecessorIds = (block) => { | ||
| const ids = []; | ||
| for (const link of collectBlocks(block, 'PredecessorLink')) { | ||
| for (const link of collectBlocks(block, '<PredecessorLink>', '</PredecessorLink>')) { | ||
| const uid = tag(link, 'PredecessorUID'); | ||
| if (/^\d+$/.test(uid)) ids.push(`msp-${uid}`); | ||
| } | ||
|
|
@@ -800,7 +779,7 @@ export function parseMsProjectXml(xml) { | |
| const day = (s) => (/^\d{4}-\d{2}-\d{2}/.test(s) ? s.slice(0, 10) : ''); | ||
| const tasks = []; | ||
| const parents = {}; // depth -> last task id at that depth | ||
| const blocks = collectBlocks(String(xml || ''), 'Task'); | ||
| const blocks = collectBlocks(String(xml || ''), '<Task>', '</Task>'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This restores the pre-#467 exact-tag scanner. Restore |
||
| for (const block of blocks) { | ||
| const uid = tag(block, 'UID'); | ||
| const name = unescape(tag(block, 'Name')); | ||
|
|
||
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,7 @@ Every user-controlled CSV cell is neutralized when, after optional leading white | |
|
|
||
| ## XML imports | ||
|
|
||
| Microsoft Project XML extraction uses bounded `indexOf`/`slice` loops. Opening and closing `Task`, `PredecessorLink`, and scalar tags accept only XML whitespace (space, tab, carriage return, or line feed) between the exact element name and `>`. Attributes, longer names, and other whitespace code points are not accepted by this deliberately narrow import profile. Dynamic regular expressions and lazy whole-document block collectors are prohibited because truncated or adversarial input can cause catastrophic backtracking. | ||
| Microsoft Project XML extraction uses bounded `indexOf`/`slice` loops. Dynamic regular expressions and lazy whole-document block collectors are prohibited because truncated or adversarial input can cause catastrophic backtracking. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The whitespace/attribute reject contract from #467 was dropped from this paragraph in the same kick that removed |
||
|
|
||
| ## Release verification | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -95,7 +95,7 @@ <h1>ScopeWeave Planner</h1> | |
|
|
||
| <input id="csv-file-input" type="file" accept=".csv,text/csv" hidden /> | ||
|
|
||
| <div id="toast" class="toast" aria-live="polite"></div> | ||
| <div id="toast" class="toast" role="status" aria-live="polite" aria-atomic="true"></div> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only remaining intended a11y delta, and it is incomplete. At
Those files are deleted on this head. Cloud |
||
|
|
||
| <div id="gantt-modal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="gantt-title" tabindex="-1"> | ||
| <div class="modal-backdrop" data-close-modal="true"></div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,9 +13,9 @@ | |
| "coverage": "npm run test:coverage", | ||
| "server": "node server/server.mjs", | ||
| "test:api": "node tests/api/auth-secret.test.mjs && node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs", | ||
| "test:unit": "node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs", | ||
| "test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/auth.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases", | ||
| "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api", | ||
| "test:unit": "node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
CI can no longer see the orchestrator regression or the claimed toast contract. Restore develop’s orchestrator entries and add the two toast files from |
||
| "test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/auth.mjs --include=server/clearfolio.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases", | ||
| "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && npm run test:api", | ||
| "test:e2e": "playwright test", | ||
| "test:e2e:headed": "playwright test --headed", | ||
| "test:e2e:cloud": "playwright install chromium && playwright test tests/e2e/cloud.spec.js", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This kick deleted the Unreleased Security line for fail-closed orchestrator briefing and the Changed line for #467 XML whitespace. The toast status line added at
129398cis also gone.Restore those shipped entries. A CI re-kick must not rewrite release truth.