# WCAG Accessibility Tools for Watch My Domains SED

A native WMD SED extension that scans domains for WCAG accessibility issues
using Puppeteer and axe-core, stores summary results in custom domain columns,
and provides an administrator-facing WCAG report page integrated into the
standard WMD SED tool UI.

---

## Overview

This extension consists of two main runtime components plus one setup script.

**Runtime tools** - ship as part of the WMD SED application:

In `tools/`:
- `wcag.php` - administrator-facing WCAG report page

In `tools/wcag/`:
- `wcag-gather.php` - CLI data gatherer
- `axe-scan.js` - Node.js / Puppeteer scanner script
- `wcag-tools-readme.md` - this file

**Setup script** - downloaded separately and run once by the administrator:
- `wcag-setup.sh` - available at `learn.domainpunch.com/static/downloads/wcag-setup.sh`

Note: `wcag.php` lives directly under `tools/` so it behaves like a standard
WMD SED tool page, while the scanner and setup-related files remain under
`tools/wcag/`.

The extension is designed so that scanning and reporting are separated:

- `wcag-gather.php` runs from the command line, scans domains, updates custom
  WMD columns, and stores detailed JSON results on disk
- `wcag.php` runs in the browser for authenticated WMD administrators and
  displays the latest saved results inside the normal WMD SED tool shell

---

## Standard Installation Layout

For a standard WMD SED installation created by the WMD SED install script:

```text
/home/wmdsed/
    sites/
        wmdsed60/                       <- WMD SED root
            tools/
                wcag.php
                ...
                wcag/
                    axe-scan.js
                    wcag-gather.php
                    wcag-tools-readme.md

    webdata/                            <- real Node/npm folder
        node_modules/                   <- Puppeteer + @axe-core/puppeteer
        websites.js                     <- existing screenshot script (untouched)
        axe-scan.js                     <- copied here by wcag-setup.sh

    logs/
        wmdsed/
            websites -> /home/wmdsed/webdata   <- symlink (used by WMD SED)
            wcag-data/                         <- created on first scan run
                example.com.json
                anotherdomain.com.json
```

Note: `websites` under the log folder is typically a symlink to
`/home/wmdsed/webdata/`. The setup script resolves this automatically.

---

## Requirements

- WMD SED already installed with shell access
- Node.js and Puppeteer already installed
  - this is normally handled automatically by the WMD SED install process
- `@axe-core/puppeteer` installed via `wcag-setup.sh` or manually
- WMD administrator access for using the browser report page

---

## Setup

### Linux / Debian / Ubuntu

#### Step 1 - Download and run `wcag-setup.sh`

Download `wcag-setup.sh` from:

`learn.domainpunch.com/static/downloads/wcag-setup.sh`

Run it as the `wmdsed` user:

```bash
bash wcag-setup.sh
```

For a standard installation, no editing is required. The script defaults to:

- `WMD_ROOT=/home/wmdsed/sites/wmdsed60`
- `NODE_DIR=/home/wmdsed/webdata`

For non-standard installations, edit the variables at the top of the script
before running it.

You can run a dry-run first:

```bash
bash wcag-setup.sh --dry-run
```

The setup script will:

1. Run pre-flight checks on all required paths
2. Install `@axe-core/puppeteer` into the Node folder if it is not already present
3. Copy `axe-scan.js` from `tools/wcag/` to the Node folder if needed
4. Optionally add a cron job for automatic scanning, depending on the script configuration

---

### Windows

There is no setup script for Windows. Complete the steps manually.

#### Step 1 - Copy `axe-scan.js` to the Node folder

```cmd
copy "C:\wmdsed\tools\wcag\axe-scan.js" "C:\wmdsed\logs\websites\axe-scan.js"
```

Adjust the paths to match your installation.

If `websites` is a symlink, use the real folder it points to, typically
something like:

```text
C:\wmdsed\webdata
```

#### Step 2 - Install `@axe-core/puppeteer`

```cmd
cd "C:\wmdsed\logs\websites"
npm install @axe-core/puppeteer --save
```

---

### All Platforms

#### Step 3 - Create custom WMD columns

Run this once:

```bash
php wcag-gather.php --setup-columns
```

This creates four custom WMD columns:

| Column | Type | Content |
|---|---|---|
| `wcag_status` | string | `pass` / `fail` / `partial` / `error` |
| `wcag_level` | string | `A` / `AA` / `AAA` / `none` |
| `wcag_issue_counts` | text | JSON such as `{"critical":2,"serious":5,"moderate":1,"minor":3}` |
| `wcag_scanned_at` | datetime | latest scan timestamp |

This command is safe to run multiple times. Existing columns are skipped.

#### Step 4 - Create the WCAG category

In WMD SED, create a category named:

**WCAG Test Domains**

Assign the domains you want scanned to this category.

#### Step 5 - Run a dry run

```bash
php wcag-gather.php --dry-run
```

This shows which domains would be scanned without actually running scans.

#### Step 6 - Run the first scan

```bash
php wcag-gather.php
```

Or with parallel workers:

```bash
php wcag-gather.php --batch=3
```

---

## Configuration

The main configuration constants are at the top of `wcag-gather.php`:

```php
const WCAG_NODE_FOLDER      = 'websites';
const WCAG_OUTPUT_FOLDER    = 'wcag-data';
const WCAG_DEFAULT_CATEGORY = 'WCAG Test Domains';
const WCAG_PROTOCOL_COLUMN  = '';
const WCAG_SKIP_RECENT_DAYS = 7;
const WCAG_LEVEL            = 'AA';
const WCAG_TIMEOUT_MS       = 30000;
const WCAG_LOCK_FILE        = 'wcag-gather.lock';
```

### Meaning of each constant

- `WCAG_NODE_FOLDER`
  - subfolder name under the WMD log path used to find the Node environment
- `WCAG_OUTPUT_FOLDER`
  - folder under the log path where full per-domain JSON scan results are stored
- `WCAG_DEFAULT_CATEGORY`
  - WMD category scanned by default
- `WCAG_PROTOCOL_COLUMN`
  - optional custom column holding the preferred protocol for each domain
- `WCAG_SKIP_RECENT_DAYS`
  - skip domains scanned within the last N days unless forced
- `WCAG_LEVEL`
  - WCAG level to test against: `A`, `AA`, or `AAA`
- `WCAG_TIMEOUT_MS`
  - Puppeteer navigation timeout per domain
- `WCAG_LOCK_FILE`
  - lock file used to prevent concurrent runs

`WCAG_OUTPUT_FOLDER` must match the constant of the same name in `wcag.php`.

---

## Protocol Resolution

If `WCAG_PROTOCOL_COLUMN` is set to a custom column name such as
`site_protocol`, the scanner uses that value for each domain.

If `WCAG_PROTOCOL_COLUMN` is empty, the scanner tries:

1. `https://`
2. then `http://` if HTTPS fails

---

## Loader Resolution

Both PHP files use `findLoader()` to locate WMD's bootstrap by walking upward
from `__DIR__`.

This means the tools do not depend on one hardcoded relative path and can still
work correctly even if the folder depth changes within the WMD installation.

---

## CLI Reference - `wcag-gather.php`

| Argument | Type | Description |
|---|---|---|
| `--category="Name"` | key=value | Override the default category |
| `--domain=example.com` | key=value | Scan one domain only; force implied |
| `--batch=3` | key=value | Run N Puppeteer processes in parallel |
| `--skip-recent=14` | key=value | Override `WCAG_SKIP_RECENT_DAYS` for this run |
| `--force` | bare flag | Re-scan all matching domains regardless of age |
| `--setup-columns` | bare flag | Create the `wcag_*` custom columns and exit |
| `--dry-run` | bare flag | Show what would be scanned without doing anything |
| `--debug` | bare flag | Verbose output including SQL and diagnostics |

### Examples

```bash
# Scan default category, skipping recently scanned domains
php wcag-gather.php

# Scan a different category
php wcag-gather.php --category="Client Sites"

# Re-scan everything regardless of age
php wcag-gather.php --force

# Scan one domain immediately
php wcag-gather.php --domain=example.com

# Scan with parallel workers
php wcag-gather.php --batch=5 --skip-recent=30

# See what would be scanned
php wcag-gather.php --dry-run --debug
```

---

## Re-scan Behaviour

| Scenario | Behaviour |
|---|---|
| `wcag_scanned_at` is empty | Always scanned |
| Scanned within `WCAG_SKIP_RECENT_DAYS` | Skipped |
| `--force` present | Always scanned |
| `--domain=...` used | Always scanned |

---

## Domain Eligibility

A domain is excluded from scanning if all three are true:

- `availability = 'available'`
- `registry_expiry` is `NULL`
- `registrar_expiry` is `NULL`

Domains with values such as `Not Available` or `possibly available` remain
eligible for scanning.

---

## Concurrency Lock

`wcag-gather.php` uses a lock file in the WMD log folder to prevent concurrent runs.

If another instance is already running:
- the second run exits immediately with a warning

If a stale lock is found:
- the script checks the PID
- removes the stale lock automatically if the original process is gone

---

## What `axe-scan.js` Does

`axe-scan.js` is invoked once per domain by `wcag-gather.php`.

It is executed from the Node folder so it resolves the local `node_modules`
correctly.

### Scanner workflow

1. Launch headless Chromium via Puppeteer
2. Navigate to the domain URL
3. Try HTTPS first, then HTTP if needed
4. Run axe-core with the configured WCAG tags
5. Output one JSON object to stdout
6. Return valid JSON even when a scan fails

### Output structure

Example:

```json
{
  "url": "https://example.com",
  "finalUrl": "https://www.example.com/",
  "scannedAt": "2026-03-26T14:32:00.000Z",
  "status": "fail",
  "wcagLevel": "A",
  "issueCounts": {
    "critical": 2,
    "serious": 5,
    "moderate": 1,
    "minor": 3
  },
  "violations": [],
  "incomplete": [],
  "passes": 42,
  "error": null
}
```

### Meaning of `status`

- `pass`
  - no violations and no incomplete items
- `partial`
  - no violations, but incomplete items exist
- `fail`
  - one or more violations found
- `error`
  - navigation or scan failed

### Meaning of `wcagLevel`

This is the highest WCAG level the page fully passes based on the scan result.

### Stored output

Full JSON files are saved to:

```text
[WMD log folder]/wcag-data/[domain].json
```

These files are later loaded on demand by `wcag.php` when an administrator opens
the popup for a domain.

---

## Browser Report - `wcag.php`

Access as an authenticated WMD administrator:

```text
https://yourserver/tools/wcag.php
```

This report page is integrated into the normal WMD SED tool shell rather than
being presented as a standalone mini-application.

### Features

- Summary stat cards
  - total
  - scanned
  - pass
  - fail
  - partial
  - error
  - not scanned

- Scope selector
  - switch between:
    - `WCAG Category Only`
    - `Show All Domains`

- Filterable table
  - filter by domain name
  - filter by status
  - filter by WCAG level

- Sortable columns
  - click any column header

- Per-row reset button
  - clears `wcag_scanned_at` for that domain
  - causes the domain to be picked up on the next gather run

- Domain detail popup
  - click any row to open the full saved result
  - violations tab
  - incomplete tab
  - affected HTML snippets
  - axe help links
  - final scanned URL
  - summary counts and pass count

- Export support
  - CSV export
  - JSON export

- Session expiry handling
  - expired browser sessions return a clear prompt to log in again

### AJAX / action endpoints handled by `wcag.php`

| `?action=` value | Behaviour |
|---|---|
| `get_violations&domain=x` | Return full saved JSON for domain `x` |
| `reset_scan&sid=x` | Clear `wcag_scanned_at` for domain sid `x` |
| `export_csv` | Download summary CSV |
| `export_json` | Download summary + detailed JSON |

---

## Scheduled Scanning

The lock file prevents overlapping runs on all platforms.

### Linux / Debian / Ubuntu

If `INSTALL_CRON="true"` in `wcag-setup.sh`, the script adds a cron job
automatically.

To add or edit manually:

```bash
crontab -e
```

Example entry:

```text
*/30 * * * * php /home/wmdsed/sites/wmdsed60/tools/wcag/wcag-gather.php --batch=3 >> /home/wmdsed/logs/wmdsed/wcag-cron.log 2>&1
```

### Windows

Use Task Scheduler.

Example batch file:

```bat
"C:\xampp\php\php.exe" "C:\wmdsed\tools\wcag\wcag-gather.php" --batch=3 >> "C:\wmdsed\logs\wcag-cron.log" 2>&1
```

Example silent wrapper VBScript:

```vbscript
Dim WinScriptHost
Set WinScriptHost = CreateObject("WScript.Shell")
WinScriptHost.Run Chr(34) & "C:\wmdsed\tools\wcag\wcag-gather.bat" & Chr(34), 0
Set WinScriptHost = Nothing
```

If your WCAG package includes `WINDOWS-SETUP.md`, `wcag-gather.xml`, or related
WMD Task Scheduler examples, follow those for the final Windows deployment.

---

## Security Notes

- `wcag-gather.php` is CLI only
  - it exits immediately if accessed from a browser

- `wcag.php` requires a valid WMD administrator session
  - non-admin users are rejected
  - expired sessions return a clear JSON or browser prompt depending on context

- The `wcag-data/` folder is stored under the WMD log path
  - it is outside the normal web root by default
  - it is not intended to be directly web-accessible

- `axe-scan.js` runs headless Chromium with `--no-sandbox`
  - ensure the OS user running the scans has appropriate permissions
  - deploy only on trusted systems

---

## Operational Notes

- The report page displays the latest saved scan state only
- Resetting a row clears `wcag_scanned_at`
- Full result JSON remains on disk until replaced by a later scan
- Export actions reflect the current scope:
  - category-only
  - or all domains

---

## Troubleshooting

### Report page says no scan data found

Check:
- `wcag-gather.php` has been run
- custom columns were created successfully
- domains exist in the configured category
- `wcag-data/` contains output JSON files

### Popup opens but shows an error

Check:
- the corresponding JSON file exists in `wcag-data/`
- file permissions allow PHP to read it
- the domain name maps to the expected filename

### Scans do not run

Check:
- `axe-scan.js` is present in the Node folder
- `@axe-core/puppeteer` is installed
- Puppeteer can launch correctly on the host
- the lock file is not stale
- the scan user has permission to access Chromium and the relevant folders

### Theme or UI issues in the report page

Check:
- `tools/js/common.js` loads before `tools/js/wcag.js`
- `wcag.js` is not duplicated
- `setTheme()` is called only once
- the active WMD color mode CSS is loading correctly

---

## Summary

This WCAG extension adds a practical accessibility workflow to WMD SED:

- scan selected domains from the command line
- store summary results directly in WMD custom columns
- keep full detailed JSON reports on disk
- review results in a WMD-integrated administrator report page
- export or re-queue domains when needed
