Skip to content

Add free-plugin foundation for Manual Issues feature#1803

Open
pattonwebz wants to merge 10 commits into
william/pro-668-extra-data-column-and-collectionfrom
william/manual-issues-free-plugin
Open

Add free-plugin foundation for Manual Issues feature#1803
pattonwebz wants to merge 10 commits into
william/pro-668-extra-data-column-and-collectionfrom
william/manual-issues-free-plugin

Conversation

@pattonwebz

Copy link
Copy Markdown
Member

Summary

This PR adds the free-plugin infrastructure that the pro Manual Issues feature will build on. It is intentionally scoped to DB schema, rescan protection, capability registration, summary counts, and hook/filter extension points — no manual-issue UI or creation flow is included here; that lives in the pro plugin.

Changes

Database (1.0.9)

  • Add source text NULL column to wp_accessibility_checker
  • Migration backfills all existing rows to source = 'automated' (text columns can't have a DB-level DEFAULT in MySQL 5.7, so dbDelta adds the column NULL and the migration fills it)

Capabilities

  • edac_create_manual_issues, edac_edit_manual_issues, edac_delete_manual_issues granted to administrator + editor on plugin activation

Rescan protection

  • edac_remove_corrected_posts() in helper-functions.php — both the pre-scan UPDATE and post-scan DELETE now filter AND (source = 'automated' OR source IS NULL), so manual rows are never touched by the scanner
  • Insert_Rule_Data::insert() SELECT query also filters to automated rows to prevent scanner from matching against a manual issue with the same rule/selector

Insert_Rule_Data

  • All INSERTs and UPDATEs now write source = 'automated' explicitly

Summary Generator

  • count_errors() and count_warnings() filter to source = 'automated' so manual issues never inflate scanner counts
  • New count_manual_issues() method; result stored as _edac_summary_manual_issues post meta
  • Manual issues do contribute to _edac_issue_density (they are real findings)

Highlighter PHP

  • wp_localize_script data for the frontend highlighter wrapped in apply_filters('edac_frontend_highlighter_app_data', $data) — pro plugin hooks in to add canCreateManualIssues, canEditManualIssues, canDeleteManualIssues
  • source and extra_data (decoded) now included in every issue object returned by the AJAX handler

Highlighter JS (@wordpress/hooks)

  • Added wp-hooks as a script dependency for the frontend highlighter bundle
  • Four wp.hooks extension points (via doAction / applyFilters):
    • edac.highlighter.init — fires after the highlighter initialises; pro registers its module here
    • edac.highlighter.menuItems — passes the <ul role="menu"> element; pro appends "Add Manual Issue"
    • edac.highlighter.issueGroups — filters the summary breakdown parts array; pro adds "X Manual"
    • edac.highlighter.issueDetail — fires after issue detail panel is rendered; pro appends Edit/Delete buttons for manual issues

Test plan

  • Activate plugin on a fresh install — source column present in DB, all edac_create/edit/delete_manual_issues caps granted to administrator and editor
  • Activate plugin on an existing install with rows — migration backfills source = 'automated' on all rows, caps granted
  • Run a page scan, confirm no new manual issue rows would appear and rescan does not delete rows with source = 'manual' (manual test with direct DB insert)
  • Confirm summary counts exclude manual rows; _edac_summary_manual_issues meta is written (value 0 with no manual rows)
  • Open frontend highlighter; confirm source and extra_data are in the AJAX response JSON
  • Confirm edac_frontend_highlighter_app_data filter fires (log in a mu-plugin hook)
  • Confirm wp.hooks extension points fire (log from a mu-plugin addAction/addFilter)

Base PR

Branches off william/pro-668-extra-data-column-and-collection (adds extra_data column). Must merge after that PR is merged.

🤖 Generated with Claude Code

- Bump DB to 1.0.9; add source text NULL column and backfill migration setting all existing rows to 'automated'
- Grant edac_create_manual_issues, edac_edit_manual_issues, edac_delete_manual_issues capabilities to administrator and editor on activation
- Protect rescan queries in edac_remove_corrected_posts() and Insert_Rule_Data::insert() from touching manual rows via source = 'automated' filter
- Insert_Rule_Data always writes source = 'automated' on INSERT and UPDATE
- Summary_Generator excludes manual issues from error/warning counts; adds _edac_summary_manual_issues post meta; manual issues contribute to density score
- Add edac_frontend_highlighter_app_data PHP filter so pro plugin can inject capability flags
- Expose source and extra_data fields in the frontend highlighter AJAX response
- Add @wordpress/hooks JS extension points: edac.highlighter.init, edac.highlighter.menuItems, edac.highlighter.issueGroups, edac.highlighter.issueDetail
- Add wp-hooks script dependency to the frontend highlighter bundle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 47fb9eb7-ae91-4b98-8892-98bc3de3383f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch william/manual-issues-free-plugin

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for manual accessibility issues in version 1.0.9, adding a source column to the database, backfilling existing records, and updating the summary generator and frontend highlighter app with new hooks and capabilities. The review feedback highlights a few critical issues: first, the new capabilities must be granted during the database update path to support existing users who update the plugin without reactivating it; second, the edac.highlighter.issueDetail hook should be wrapped inside the issueContent null check to prevent runtime JavaScript errors; and finally, count_contrast_errors() should be updated to filter by source to ensure consistent error calculations.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +99 to +101
if ( version_compare( $db_version, '1.0.9', '<' ) ) {
$this->migrate_source_column( $table_name );
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The new capabilities (edac_create_manual_issues, edac_edit_manual_issues, edac_delete_manual_issues) are currently only granted in edac_activation() inside includes/activation.php.

However, in WordPress, register_activation_hook only runs when a plugin is explicitly activated or reactivated, and not when a plugin is updated. This means existing users who update the plugin to version 1.0.9 will receive the database schema updates (via Update_Database::edac_update_database()), but will not have the new capabilities granted to their roles, breaking the manual issues feature for them.

To ensure all existing users receive these capabilities upon updating, we should also execute the capability-granting logic within the database update/migration path when upgrading to version 1.0.9.

			if ( version_compare( $db_version, '1.0.9', '<' ) ) {
				$this->migrate_source_column( $table_name );
				$this->grant_manual_issues_capabilities();
			}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — this is a real gap. Activation hook doesn't fire on updates so existing users would have missed the caps.

Fixed in commit a0ce7d1, but with a different approach than suggested: rather than a private method with hardcoded cap names, the migration calls edac_get_plugin_capabilities() with the same loop used in edac_activation(). Since the migration runs on admin_init (after plugins_loaded), the pro plugin's edac_plugin_capabilities filter callback is already registered, so pro caps are included automatically. This keeps the free/pro boundary intact — the free plugin never needs to know which specific caps the pro plugin defines.

Comment on lines +143 to +153
private function migrate_source_column( string $table_name ): void {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time migration query.
$wpdb->query(
$wpdb->prepare(
'UPDATE %i SET source = %s WHERE source IS NULL',
$table_name,
'automated'
)
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Add the helper method grant_manual_issues_capabilities() to update user roles with the new capabilities during the 1.0.9 database migration.

	private function migrate_source_column( string $table_name ): void {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time migration query.
		$wpdb->query(
			$wpdb->prepare(
				'UPDATE %i SET source = %s WHERE source IS NULL',
				$table_name,
				'automated'
			)
		);
	}

	/**
	 * Grant manual issues capabilities to administrator and editor.
	 *
	 * @since 1.0.9
	 * @return void
	 */
	private function grant_manual_issues_capabilities(): void {
		$manual_caps = [
			'edac_create_manual_issues',
			'edac_edit_manual_issues',
			'edac_delete_manual_issues',
		];
		foreach ( [ 'administrator', 'editor' ] as $role_name ) {
			$role = get_role( $role_name );
			if ( $role ) {
				foreach ( $manual_caps as $cap ) {
					$role->add_cap( $cap );
				}
			}
		}
	}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the same commit a0ce7d1 — see reply on the companion comment above for the approach taken.

Comment on lines 1372 to 1387
if ( issueContent ) {
issueContent.style.display = 'block';
}

/**
* Fires after the issue detail panel has been rendered.
*
* Pro plugin uses this to append Edit / Delete buttons for manual issues.
*
* @since 1.0.9
*
* @param {Object} issue The issue data object.
* @param {HTMLElement} contentEl The `.edac-highlight-panel-controls-content-issue` element.
*/
doAction( 'edac.highlighter.issueDetail', matchingObj, issueContent );
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The doAction( 'edac.highlighter.issueDetail', matchingObj, issueContent ) call is currently executed outside of the if ( issueContent ) check.

If issueContent is null or undefined (e.g., if the DOM element is not found), doAction will still be invoked, passing issueContent as null. Any hooks registered to edac.highlighter.issueDetail in the Pro plugin that attempt to manipulate or append elements to contentEl will throw a JS TypeError (e.g., "Cannot read properties of null").

Moving the doAction call inside the if ( issueContent ) block prevents this potential runtime error.

Suggested change
if ( issueContent ) {
issueContent.style.display = 'block';
}
/**
* Fires after the issue detail panel has been rendered.
*
* Pro plugin uses this to append Edit / Delete buttons for manual issues.
*
* @since 1.0.9
*
* @param {Object} issue The issue data object.
* @param {HTMLElement} contentEl The `.edac-highlight-panel-controls-content-issue` element.
*/
doAction( 'edac.highlighter.issueDetail', matchingObj, issueContent );
}
if ( issueContent ) {
issueContent.style.display = 'block';
/**
* Fires after the issue detail panel has been rendered.
*
* Pro plugin uses this to append Edit / Delete buttons for manual issues.
*
* @since 1.0.9
*
* @param {Object} issue The issue data object.
* @param {HTMLElement} contentEl The `.edac-highlight-panel-controls-content-issue` element.
*/
doAction( 'edac.highlighter.issueDetail', matchingObj, issueContent );
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in commit 912ffd4. Moved doAction inside the if (issueContent) block so the action only fires when the element exists. Previously it fired unconditionally, meaning any pro hook that tried to append to contentEl when it was null would throw a TypeError.

Comment on lines 78 to 79
$summary['contrast_errors'] = $this->count_contrast_errors();
$summary['errors'] -= $summary['contrast_errors'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While count_errors() and count_warnings() have been updated to filter by (source = 'automated' OR source IS NULL), count_contrast_errors() has not been updated.

If a manual issue is created with the color_contrast_failure rule, it will be counted by count_contrast_errors() (since it doesn't filter by source), but not by count_errors() (since it filters out 'manual'). When $summary['errors'] -= $summary['contrast_errors']; is executed, the manual contrast error will be subtracted from the automated errors, potentially resulting in a negative or incorrect error count.

Please update count_contrast_errors() to also filter by (source = 'automated' OR source IS NULL) to maintain consistency.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specific arithmetic bug described here can't occur with our data model. Manual issues use rule = 'manual_issue' as their sentinel — they are never inserted with rule = 'color_contrast_failure'. So count_contrast_errors() will never count a manual issue regardless of source filtering.

The review also states that count_errors() and count_warnings() were updated to filter by source — they weren't; class-summary-generator.php was not touched in this PR.

That said, there is a real related issue: count_errors() and count_warnings() both use ruletype = 'error'/'warning' with no source filter, so they would count manual issues once the pro plugin enables creating them. That's intentionally out of scope here since no manual issues can be created yet. It will need to be addressed in the pro plugin PR before manual issues ship.

pattonwebz and others added 9 commits June 28, 2026 01:29
New parser will replace x.x.x with the actual version number at release time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove summary-generator changes (count filtering, manual count method,
_edac_summary_manual_issues meta, density change) — pro plugin owns these.

Remove edac.highlighter.init and edac.highlighter.issueGroups hook points
— not load-bearing; pro plugin can manage its own init and count display.

Keeps: source column + migration, capabilities, rescan protection,
Insert_Rule_Data source tagging, edac_frontend_highlighter_app_data filter,
source/extra_data in AJAX response, menuItems and issueDetail JS hooks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Issues_Query: exclude manual rows from all count/distinct/get_ids queries
  via AND (source = 'automated' OR source IS NULL) in where_base; free
  plugin has no filter hook for callers to override these queries themselves
- Frontend highlighter AJAX: resolve screenshot_url inline from
  extra_data.screenshot_id so pro JS has no need for a second round-trip
- JS: re-add edac.highlighter.init action (pro needs a clean entry point
  to register its module after the highlighter is ready)
- JS: re-add edac.highlighter.issueGroups filter (pro JS uses this to
  append a Manual count to the panel footer summary line; DOM manipulation
  after the fact would be fragile)
- Deactivation: remove edac_create/edit/delete_manual_issues caps from
  administrator and editor on plugin deactivation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pro plugin JS can resolve attachment URLs via the existing WP REST
endpoint (/wp/v2/media/{id} → source_url) using its own apiFetch call.
extra_data.screenshot_id is already returned decoded, so the pro plugin
has everything it needs without the free plugin knowing about screenshots.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces a single filterable function that maps cap slugs to their
default roles. Both activation and deactivation loop over it, so:
- Adding a new cap means updating one place only
- Pro plugin (or any add-on) hooks edac_plugin_capabilities to register
  additional caps through the same activate/deactivate lifecycle
- No hardcoded role loops or cap lists scattered across multiple files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Free plugin edac_get_plugin_capabilities() returns an empty array;
pro plugin registers edac_create/edit/delete_manual_issues via the
edac_plugin_capabilities filter. Free plugin owns the generic
activate/deactivate lifecycle, pro plugin owns its own cap definitions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
register_activation_hook does not fire on plugin updates, only on
explicit activation. Existing users upgrading to 1.0.9 received the
source column migration but not the capability grants. Calling
grant_plugin_capabilities() inside the 1.0.9 migration block fixes this.

Uses edac_get_plugin_capabilities() rather than hardcoded cap names so
the pro plugin's caps (registered via edac_plugin_capabilities filter at
plugins_loaded) are included when the migration runs on admin_init.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
doAction was firing even when issueContent was null (element not found
in DOM), passing null as the contentEl argument. Pro plugin hooks that
append elements to contentEl would throw TypeError. Moving the call
inside the if(issueContent) block ensures the element exists before
any hook callbacks receive it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
count_errors() and count_warnings() in Summary_Generator now filter
AND (source = 'automated' OR source IS NULL) so that pro plugin
manual issues are not counted in the automated scan totals.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant