Add free-plugin foundation for Manual Issues feature#1803
Conversation
- 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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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.
| if ( version_compare( $db_version, '1.0.9', '<' ) ) { | ||
| $this->migrate_source_column( $table_name ); | ||
| } |
There was a problem hiding this comment.
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();
}There was a problem hiding this comment.
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.
| 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' | ||
| ) | ||
| ); | ||
| } |
There was a problem hiding this comment.
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 );
}
}
}
}There was a problem hiding this comment.
Addressed in the same commit a0ce7d1 — see reply on the companion comment above for the approach taken.
| 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 ); | ||
| } |
There was a problem hiding this comment.
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.
| 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 ); | |
| } |
There was a problem hiding this comment.
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.
| $summary['contrast_errors'] = $this->count_contrast_errors(); | ||
| $summary['errors'] -= $summary['contrast_errors']; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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>
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)
source text NULLcolumn towp_accessibility_checkersource = '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_issuesgranted to administrator + editor on plugin activationRescan protection
edac_remove_corrected_posts()inhelper-functions.php— both the pre-scan UPDATE and post-scan DELETE now filterAND (source = 'automated' OR source IS NULL), so manual rows are never touched by the scannerInsert_Rule_Data::insert()SELECT query also filters to automated rows to prevent scanner from matching against a manual issue with the same rule/selectorInsert_Rule_Data
source = 'automated'explicitlySummary Generator
count_errors()andcount_warnings()filter tosource = 'automated'so manual issues never inflate scanner countscount_manual_issues()method; result stored as_edac_summary_manual_issuespost meta_edac_issue_density(they are real findings)Highlighter PHP
wp_localize_scriptdata for the frontend highlighter wrapped inapply_filters('edac_frontend_highlighter_app_data', $data)— pro plugin hooks in to addcanCreateManualIssues,canEditManualIssues,canDeleteManualIssuessourceandextra_data(decoded) now included in every issue object returned by the AJAX handlerHighlighter JS (
@wordpress/hooks)wp-hooksas a script dependency for the frontend highlighter bundlewp.hooksextension points (viadoAction/applyFilters):edac.highlighter.init— fires after the highlighter initialises; pro registers its module hereedac.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 issuesTest plan
sourcecolumn present in DB, alledac_create/edit/delete_manual_issuescaps granted to administrator and editorsource = 'automated'on all rows, caps grantedsource = 'manual'(manual test with direct DB insert)_edac_summary_manual_issuesmeta is written (value 0 with no manual rows)sourceandextra_dataare in the AJAX response JSONedac_frontend_highlighter_app_datafilter fires (log in a mu-plugin hook)wp.hooksextension points fire (log from a mu-pluginaddAction/addFilter)Base PR
Branches off
william/pro-668-extra-data-column-and-collection(addsextra_datacolumn). Must merge after that PR is merged.🤖 Generated with Claude Code