diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..94afdcac --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,145 @@ +# WordPress.org Two-Factor Plugin — Copilot Agent Instructions + +> See also `AGENTS.md` in the repository root for additional context shared across all AI agents. + +## Project Overview + +This is a **security-critical** WordPress plugin that customizes the [Two-Factor](https://github.com/WordPress/two-factor) plugin for WordPress.org. It enforces 2FA on privileged accounts (committers, deputies, theme authors, WordCamp organizers), strips capabilities from users who haven't enabled 2FA, and provides a React-based settings UI with REST API endpoints. + +Key subsystems: provider management (WebAuthn/TOTP/Backup Codes), capability enforcement, session revalidation ("sudo mode"), encrypted TOTP secrets, and a block-based settings interface. + +## Working on Issues + +### Before Writing Code + +1. **Read the issue thoroughly.** Understand the problem, reproduction steps, and acceptance criteria. +2. **Explore the relevant code.** Read the files involved — don't guess at implementations. +3. **Understand _why_ the code is the way it is.** Check `git log` and `git blame` for the files you'll change. Read commit messages and linked PRs/issues to understand the decisions that led to the current design. Security-related decisions are especially important — do not undo them without understanding the rationale. +4. **Check for related tests.** Look in `tests/` for existing test coverage of the area you're modifying. +5. **Map the dependency chain.** This plugin depends on the Two-Factor core plugin, the WebAuthn provider plugin, bbPress, Gutenberg, and wporg-mu-plugins. Understand which dependencies are involved in your change. + +### Writing Code + +Follow **WordPress coding standards** strictly: +- PHP: tabs for indentation, Yoda conditions (`'value' === $var`), snake_case functions, braces on same line. +- JS/React: tabs for indentation, follow wp-scripts/eslint conventions. +- When in doubt, match the existing file's style and adhere to the WordPress coding standards above. + +**Architecture rules:** +- The main plugin file (`wporg-two-factor.php`) uses the `WordPressdotorg\Two_Factor` namespace. +- Settings UI lives in `settings/` — it's a `@wordpress/scripts` block package with its own `package.json`. +- REST API endpoints are in `settings/rest-api.php`. +- Session revalidation is in `revalidation/index.php`. +- Custom providers: `class-encrypted-totp-provider.php` (TOTP with encryption), `class-wporg-webauthn-provider.php` (WebAuthn with caching). + +**Security considerations:** +- This is a security plugin. Every change must be defensively coded. +- Never weaken capability checks, 2FA enforcement, or session validation. +- TOTP secrets are encrypted at rest — maintain this guarantee. +- User input must be sanitized, output must be escaped. +- Do not introduce OWASP Top 10 vulnerabilities. + +### Writing Tests + +Every PR **must** include tests for the changes. This is a security plugin — untested code is unacceptable. + +**PHP unit tests:** +- Location: `tests/` directory, files prefixed with `test-`. +- Framework: PHPUnit 9.6 with WordPress test utilities (`WP_UnitTestCase`). +- Run: `npm test` (runs PHPUnit inside wp-env). +- Coverage target: 100% for meaningful, testable code. Use `@codeCoverageIgnore` pragmatically (as configured in `phpunit.xml.dist`) to exclude non-behavioral glue, unreachable or environment-specific paths, but never to hide untested business logic. +- **NEVER use `remove_all_filters()` or `remove_all_actions()` in tests** — it removes production callbacks. Always add/remove specific callbacks by reference. +- Test classes extend `WP_UnitTestCase`. Use `wpSetUpBeforeClass` for expensive setup, `tear_down` for cleanup. +- The test bootstrap (`tests/bootstrap.php`) mocks WordPress.org-specific functions. + +**JavaScript tests:** +- Location: `settings/src/tests/`. +- Framework: Jest via `@wordpress/scripts`. +- Run: `npm run test:js`. +- Uses `@testing-library/react` for component tests. + +**End-to-end tests with Playwright:** +- Use the Playwright MCP server to interact with the local WordPress site in the browser. +- wp-env provides two instances: + - **Dev instance:** `http://localhost:8888` — use this for Playwright browser testing. + - **Test instance:** `http://localhost:8889` — used by PHPUnit (no browser testing here). +- **Login credentials:** username `admin`, password `password` (wp-env defaults). +- **Login URL:** `http://localhost:8888/wp-login.php`. Log in before testing authenticated flows. +- The dev environment has a **Dummy 2FA provider** enabled (see `.wp-env/mu-plugins/mu-plugin.php`), which allows completing 2FA login without real authenticator hardware. Use this for Playwright flows that require passing the 2FA prompt. +- The `admin` user is configured as a super admin and "special user" in the mu-plugin, so 2FA enforcement applies to them. +- This is a **multisite** installation with bbPress, Gutenberg, and the Two-Factor plugins active network-wide. +- Test real user flows: enabling 2FA, verifying enforcement, checking the settings UI, revalidation prompts. +- Take screenshots to verify visual state when relevant. +- The user profile / 2FA settings page is at `http://localhost:8888/support/users/admin/edit/account/` (bbPress user edit page). + +### Validating Changes + +Before opening a PR, verify your changes pass all checks: + +1. **PHP tests:** `npm test` +2. **JS tests:** `npm run test:js` +3. **JS linting:** `npm run lint:js` +4. **PHP linting:** `npx wp-env run cli --env-cwd=wp-content/plugins/wporg-two-factor composer lint` +5. **E2E verification (mandatory):** Use Playwright to verify the change works in the browser. This is required for every PR — even if the change seems low-risk, open the affected pages and confirm there are no regressions. For UI changes, take screenshots to document the result. + +If any check fails, fix the issue — do not skip or ignore failures. + +## Project Layout + +``` +wporg-two-factor.php # Main plugin entry point (namespace: WordPressdotorg\Two_Factor) +class-encrypted-totp-provider.php # TOTP provider with at-rest encryption +class-wporg-webauthn-provider.php # WebAuthn provider with caching +stats.php # 2FA adoption analytics +settings/ + settings.php # Settings page registration, replaces core 2FA UI + rest-api.php # REST endpoints for TOTP setup, provider status, passwords + package.json # @wordpress/scripts block package + src/ # React components for settings UI + components/ # UI components (TOTP, passwords, backup codes, WebAuthn) + tests/ # Jest tests for React components +revalidation/ + index.php # Session revalidation / "sudo mode" system +tests/ + bootstrap.php # Test bootstrap with WordPress.org mocks + test-wporg-two-factor.php # Main PHP test suite + settings/ + test-rest-api.php # REST API endpoint tests +.wp-env.json # wp-env configuration — plugins MUST use GitHub repo refs (not zip downloads) for Copilot agents +.wp-env/ + after-start.sh # Lifecycle script: composer install, plugin activation, bbPress config + mu-plugins/ # Mock mu-plugins for local development +``` + +## Build & Test Commands + +| Task | Command | +|---|---| +| Start dev environment | `npx wp-env start` | +| Run PHP tests | `npm test` | +| Run JS tests | `npm run test:js` | +| Lint JS | `npm run lint:js` | +| Lint PHP | `npx wp-env run cli --env-cwd=wp-content/plugins/wporg-two-factor composer lint` | +| Build settings block | `npm run build --workspaces` | +| WP-CLI in dev env | `npx wp-env run cli wp ` | + +## Commit Message Style + +Follow the existing style visible in `git log`. Use a short imperative subject line with a category prefix when appropriate (e.g., "WebAuthN:", "Revalidation:", "Tests:", "Build:"). Keep messages concise and focused on _why_, not _what_. + +## Key Dependencies + +- **Two-Factor** (`WordPress/two-factor`): Core 2FA framework — provides `Two_Factor_Core`, `Two_Factor_Totp`, `Two_Factor_Backup_Codes`. +- **Two-Factor WebAuthn** (`two-factor-provider-webauthn`): WebAuthn provider — provides `TwoFactor_Provider_WebAuthn`. +- **bbPress**: Forum plugin — user profiles integrate with 2FA settings. +- **wporg-mu-plugins**: WordPress.org shared utilities including encryption functions. +- **Gutenberg**: Block editor — settings UI is a block. + +## What NOT to Do + +- Do not refactor code you weren't asked to change. +- Do not add features beyond what the issue requests. +- Do not weaken security checks or enforcement for convenience. +- Do not skip or disable pre-commit hooks or linting. +- Do not introduce new dependencies without strong justification. +- Do not change CI/CD workflows unless the issue specifically requires it. diff --git a/.github/instructions/js.instructions.md b/.github/instructions/js.instructions.md new file mode 100644 index 00000000..e5b59524 --- /dev/null +++ b/.github/instructions/js.instructions.md @@ -0,0 +1,13 @@ +--- +applyTo: "settings/**/*.{js,jsx,ts,tsx}" +--- + +JavaScript/React conventions for the settings UI: + +- This is a `@wordpress/scripts` block package. Use WordPress JS coding standards. +- Tabs for indentation. +- Import WordPress packages from `@wordpress/*` (e.g., `@wordpress/element`, `@wordpress/api-fetch`, `@wordpress/i18n`). +- Use `apiFetch` for REST API calls, not raw `fetch`. +- Translatable strings use `__()`, `_n()`, `sprintf()` from `@wordpress/i18n`. +- Jest tests live in `settings/src/tests/` and use `@testing-library/react`. +- Run JS tests with `npm run test:js`, lint with `npm run lint:js`. diff --git a/.github/instructions/php.instructions.md b/.github/instructions/php.instructions.md new file mode 100644 index 00000000..d1609582 --- /dev/null +++ b/.github/instructions/php.instructions.md @@ -0,0 +1,12 @@ +--- +applyTo: "**/*.php" +--- + +Follow WordPress PHP coding standards and WordPress documentation standards: +- Tabs for indentation, never spaces. +- Snake_case for function and variable names. +- Opening braces on the same line as the statement. +- Space inside parentheses: `if ( $condition )`, `function_call( $arg )`. +- Use strict type comparisons (`===`, `!==`) unless there's a specific reason not to. +- Sanitize all input, escape all output. Use `sanitize_*()`, `esc_html()`, `esc_attr()`, `wp_kses()` as appropriate. +- Prefix functions and hooks with the plugin namespace or use the `WordPressdotorg\Two_Factor` namespace. diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md new file mode 100644 index 00000000..b955bbeb --- /dev/null +++ b/.github/instructions/tests.instructions.md @@ -0,0 +1,15 @@ +--- +applyTo: "tests/**" +--- + +Testing rules for this security plugin: + +- Every code change requires corresponding test coverage. +- Test classes extend `WP_UnitTestCase`. +- Use `wpSetUpBeforeClass( WP_UnitTest_Factory $factory )` for expensive setup shared across tests. +- Use `tear_down()` to reset globals and state after each test. +- NEVER use `remove_all_filters()` or `remove_all_actions()`. Always save callbacks to a variable and remove the specific callback. +- Use `@covers` annotations on every test method to track coverage. +- Test both positive and negative cases — especially for security enforcement (e.g., verify that capability stripping works AND that it doesn't affect users who have 2FA enabled). +- The bootstrap file (`tests/bootstrap.php`) provides mock WordPress.org functions. Check what's available before creating new mocks. +- Run tests with `npm test` which executes PHPUnit inside the wp-env Docker container. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..3e344fff --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,78 @@ +name: "Copilot Setup Steps" + +on: workflow_dispatch + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + permissions: + contents: read + timeout-minutes: 30 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer:v2 + + - name: Cache node_modules + uses: actions/cache@v4 + id: cache-node-modules + with: + path: | + node_modules + settings/node_modules + key: node-modules-${{ hashFiles('package-lock.json', 'settings/package-lock.json') }} + + - name: Install NPM dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: npm install + + - name: Cache Docker images + uses: actions/cache@v4 + id: cache-docker + with: + path: /tmp/wp-env-docker + key: docker-wp-env-${{ hashFiles('.wp-env.json') }} + + - name: Load cached Docker images + if: steps.cache-docker.outputs.cache-hit == 'true' + run: | + shopt -s nullglob + for f in /tmp/wp-env-docker/*.tar; do + docker load < "$f" + done + + # Docker is pre-installed on ubuntu-latest runners. + # wp-env uses Docker to run WordPress + MySQL containers. + - name: Start wp-env + run: npx wp-env start --xdebug=coverage + + - name: Save Docker images to cache + if: steps.cache-docker.outputs.cache-hit != 'true' + run: | + mkdir -p /tmp/wp-env-docker + images=$(docker ps -a --format '{{.Image}} {{.Names}}' | awk '$2 ~ /^wp-env/ {print $1}' | sort -u) + for image in $images; do + filename=$(echo "$image" | tr '/:' '_') + docker save "$image" -o "/tmp/wp-env-docker/${filename}.tar" + done + + - name: Verify environment is healthy + run: | + # Verify WordPress is responding + curl -sf http://localhost:8888/ > /dev/null + curl -sf http://localhost:8889/ > /dev/null + # Run test suites to confirm everything works + npx wp-env run tests-cli --env-cwd=wp-content/plugins/wporg-two-factor vendor/bin/phpunit + npm run test:js + npm run lint:js diff --git a/.gitignore b/.gitignore index 0a7d8870..56933a22 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ tests/.phpunit.result.cache tests/coverage vendor node_modules +.claude/settings.local.json diff --git a/.nvmrc b/.nvmrc index 3c032078..a45fd52c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 +24 diff --git a/.wp-env.json b/.wp-env.json index 945c1d02..ac62b7d1 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,10 +1,11 @@ { + "core": "https://github.com/WordPress/WordPress/archive/refs/heads/master.zip", "multisite": true, "plugins": [ - "https://downloads.wordpress.org/plugin/gutenberg.latest-stable.zip", - "https://downloads.wordpress.org/plugin/bbpress.latest-stable.zip", + "WordPress/gutenberg", + "bbpress/bbPress", "WordPress/two-factor", - "https://downloads.wordpress.org/plugin/two-factor-provider-webauthn.latest-stable.zip", + "https://github.com/sjinks/wp-two-factor-provider-webauthn/releases/latest/download/two-factor-provider-webauthn.zip", "." ], "mappings": { diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..dde1fcc8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,125 @@ +# Agent Instructions for wporg-two-factor + +> Copilot-specific instructions (Playwright, wp-env URLs, login credentials) are in `.github/copilot-instructions.md`. + +## Overview + +WordPress.org-specific customizations for the [Two Factor](https://github.com/WordPress/two-factor) plugin. This is a standard WordPress network-activated plugin that extends the upstream Two Factor plugin with encrypted TOTP, WebAuthn support, a custom React-based settings UI (Gutenberg block), session revalidation, and capability restrictions for privileged users without 2FA. + +**Languages:** PHP (WordPress plugin, ~8 source files), JavaScript/React (Gutenberg block in `settings/`). +**Runtime:** PHP 7.4+ (CI uses 7.4), Node 24+ (`.nvmrc`, CI uses 20), Docker (for wp-env test environment). +**Namespace:** `WordPressdotorg\Two_Factor` + +## Repository Layout + +``` +wporg-two-factor.php # Main plugin file — hooks, filters, capability logic +class-encrypted-totp-provider.php # Extends Two_Factor_Totp with encryption +class-wporg-webauthn-provider.php # Extends WebAuthn provider with caching/filters +stats.php # WordPress.org stats tracking +revalidation/index.php # 2FA session revalidation ("sudo mode") +settings/ # Gutenberg block workspace (npm workspace) + settings.php # Block registration, custom UI replacement + rest-api.php # REST API endpoints and user fields + src/ # React source (components/, hooks/, tests/, utilities/) + block.json # Block metadata + render.php # Block server-side render callback + build/ # Compiled output (gitignored) + package.json # Workspace package with wp-scripts + jest.config.js, jest.setup.js, babel.config.json +tests/ + bootstrap.php # PHPUnit bootstrap — loads plugins in wp-env context + test-wporg-two-factor.php # PHP tests for main plugin + settings/ # PHP tests for REST API and application passwords +.wp-env.json # wp-env config (multisite dev, single-site test install) +.wp-env/after-start.sh # Lifecycle script: composer install, plugin/theme activation, bbPress config +phpcs.xml # PHPCS config: WordPress-Core/Docs/Extra with exclusions +phpunit.xml.dist # PHPUnit config (multisite, prefix "test-") +composer.json # PHP deps: phpcs, phpunit, wpcs, polyfills +package.json # Root: wp-env, npm workspaces ["settings"] +``` + +## Build & Validate Commands + +Always run `npm install` first. This installs both root and workspace (`settings/`) dependencies. + +### Bootstrap + +```sh +npm install # Install all Node dependencies (root + workspaces) +composer install # Install PHP dependencies (phpcs, phpunit, etc.) +``` + +### Build (JavaScript) + +```sh +npm run build --workspaces # Builds settings/ block → settings/build/ +``` + +### Local Development Environment (requires Docker) + +```sh +npx wp-env start # Starts WordPress + test containers (~37s) +# Dev site: http://localhost:8888 | Test site: http://localhost:8889 +npx wp-env stop # Stops containers +``` + +### Testing + +**PHP tests** require a running wp-env Docker environment: + +```sh +npx wp-env start # Must be running first +npm test # Runs PHPUnit inside the tests-cli container (~1s) +``` + +**JavaScript tests** do NOT require Docker: + +```sh +npm run test:js # Runs Jest tests in settings/ workspace (~3s) +``` + +### Linting + +**JavaScript lint** (enforced in CI — must pass with 0 errors): + +```sh +npm run lint:js # ESLint via wp-scripts on settings/src/ +``` + +**PHP lint** (NOT enforced in CI — has pre-existing violations): + +```sh +composer run lint # PHPCS with WordPress coding standards +composer run format # Auto-fix with PHPCBF +``` + +## CI Checks (GitHub Actions on PRs) + +Two workflows run on every pull request (both must pass), plus a build workflow on trunk: + +1. **`.github/workflows/lint.yml`** — Runs `npm run lint:js`. JS lint errors (not warnings) will fail the build. + +2. **`.github/workflows/test.yml`** — Starts wp-env, then runs `npm test` (PHP) and `npm run test:js` (JS). Both must pass. + +3. **`.github/workflows/build.yml`** — Runs only on trunk push (not PRs). Builds and pushes to the `build` branch. + +## Key Conventions + +- **WordPress Coding Standards.** Use tabs for indentation. PHP follows WordPress-Core style (see `phpcs.xml` for customized rules). Text domain is `wporg`. +- **PHP test files** are prefixed `test-` and placed in `tests/`. Test classes extend `WP_UnitTestCase`. PHPUnit runs as **multisite** per `phpunit.xml.dist`. +- **JS test files** use `*.test.js` convention inside `settings/src/tests/`. +- **Never use `remove_all_filters()` or `remove_all_actions()` in tests.** Always save specific callbacks and add/remove them individually. +- **The `settings/` directory is an npm workspace.** JS build/lint/test commands are delegated via `--workspaces` or `-w settings`. +- **Dependencies not obvious from layout:** The plugin depends on `WordPress/two-factor`, `two-factor-provider-webauthn`, `WordPress/wporg-mu-plugins` (build branch), `bbpress`, `gutenberg`, and themes `wporg-parent-2021` (build branch) and `wporg-support-2024` — all mapped via `.wp-env.json`. + +## Validation Checklist + +Before submitting changes, always verify: + +1. `npm run lint:js` exits with 0 errors (warnings are acceptable) +2. `npm run test:js` — all JS tests pass +3. `npm test` — all PHP tests pass (requires `npx wp-env start`) +4. `npm run build --workspaces` — JS build succeeds without errors + +Trust these instructions. Only search the codebase if something here is incomplete or produces unexpected errors. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md