Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion accessibility-checker.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

// Current database version.
if ( ! defined( 'EDAC_DB_VERSION' ) ) {
define( 'EDAC_DB_VERSION', '1.0.8' );
define( 'EDAC_DB_VERSION', '1.0.9' );
}

// Plugin Folder Path.
Expand Down
4 changes: 3 additions & 1 deletion admin/class-frontend-highlight.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public function get_issues( $post_id ) {
$table_name = $wpdb->prefix . 'accessibility_checker';
$post_id = (int) $post_id;
$siteid = get_current_blog_id();
$results = $wpdb->get_results( $wpdb->prepare( 'SELECT id, rule, ignre, object, ruletype, selector, ancestry, xpath, landmark, landmark_selector FROM %i where postid = %d and siteid = %d', $table_name, $post_id, $siteid ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe variable used for table name.
$results = $wpdb->get_results( $wpdb->prepare( 'SELECT id, rule, ignre, object, ruletype, selector, ancestry, xpath, landmark, landmark_selector, source, extra_data FROM %i where postid = %d and siteid = %d', $table_name, $post_id, $siteid ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe variable used for table name.
if ( ! $results ) {
return null;
}
Expand Down Expand Up @@ -149,6 +149,8 @@ public function ajax() {
$array['severity'] = $rule[0]['severity'] ?? '';
$array['landmark'] = $result['landmark'] ?? '';
$array['landmark_selector'] = $result['landmark_selector'] ?? '';
$array['source'] = $result['source'] ?? 'automated';
$array['extra_data'] = isset( $result['extra_data'] ) ? json_decode( $result['extra_data'], true ) : null;

$issues[] = $array;

Expand Down
5 changes: 4 additions & 1 deletion admin/class-insert-rule-data.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public function insert( object $post, string $rule, string $ruletype, string $ru
'ruletype' => $ruletype,
'object' => esc_attr( $rule_obj ),
'extra_data' => $extra_data,
'source' => 'automated',
'recordcheck' => 1,
'user' => get_current_user_id(),
'ignre' => 0,
Expand All @@ -88,7 +89,7 @@ public function insert( object $post, string $rule, string $ruletype, string $ru
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation.
$results = $wpdb->get_results(
$wpdb->prepare(
'SELECT postid, ignre FROM %i where type = %s and postid = %d and rule = %s and selector = %s and siteid = %d',
"SELECT postid, ignre FROM %i WHERE type = %s AND postid = %d AND rule = %s AND selector = %s AND siteid = %d AND (source = 'automated' OR source IS NULL)",
$table_name,
$rule_data['type'],
$rule_data['postid'],
Expand Down Expand Up @@ -124,6 +125,7 @@ public function insert( object $post, string $rule, string $ruletype, string $ru
'xpath' => $rule_data['xpath'],
'ignre' => $rule_data['ignre'],
'extra_data' => self::encode_extra_data( is_array( $rule_data['extra_data'] ) ? $rule_data['extra_data'] : null ),
'source' => 'automated',
],
[
'siteid' => $rule_data['siteid'],
Expand Down Expand Up @@ -174,6 +176,7 @@ public function insert( object $post, string $rule, string $ruletype, string $ru
'ruletype' => sanitize_text_field( $rule_data['ruletype'] ),
'object' => esc_attr( $rule_data['object'] ),
'extra_data' => self::encode_extra_data( is_array( $extra_data_raw ) ? $extra_data_raw : null ),
'source' => 'automated',
'recordcheck' => absint( $rule_data['recordcheck'] ),
'user' => absint( $rule_data['user'] ),
'ignre' => absint( $rule_data['ignre'] ),
Expand Down
4 changes: 4 additions & 0 deletions admin/class-issues-query.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ public function __construct( $filter = [], $record_limit = 100000, $flags = self
$this->query['where_base'] = $wpdb->prepare( 'WHERE siteid=%d and ignre=%d and ignre_global=%d ', [ $siteid, 0, 0 ] );
}

// Exclude manual issues from all scanner counts; source IS NULL covers rows
// inserted before the 1.0.9 migration backfill has run.
$this->query['where_base'] .= " AND (source = 'automated' OR source IS NULL)";

$filter_defaults = [
'post_types' => [],
'rule_types' => [],
Expand Down
56 changes: 56 additions & 0 deletions admin/class-update-database.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public function edac_update_database() {
ruletype text NOT NULL,
object mediumtext NOT NULL,
extra_data text NULL,
source text NULL,
recordcheck mediumint(9) NOT NULL,
created timestamp NOT NULL default CURRENT_TIMESTAMP,
user bigint(20) NOT NULL,
Expand Down Expand Up @@ -92,6 +93,15 @@ public function edac_update_database() {

// 1.0.8: Added extra_data column. dbDelta() handles ADD COLUMN automatically
// when the column appears in the CREATE TABLE DDL above; no data migration required.

// 1.0.9: Added source column. Backfill existing rows to 'automated' since text
// columns cannot carry a DB-level DEFAULT in MySQL 5.7. Also grant plugin
// capabilities here so existing users who update (rather than reactivate)
// receive them — register_activation_hook does not fire on plugin updates.
if ( version_compare( $db_version, '1.0.9', '<' ) ) {
$this->migrate_source_column( $table_name );
$this->grant_plugin_capabilities();
}
Comment on lines +101 to +104

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.

}

// Update database version option.
Expand Down Expand Up @@ -122,6 +132,52 @@ private function migrate_license_key_to_shared_option() {
delete_option( 'edac_license_key' );
}

/**
* Backfill existing rows so every row has source = 'automated'.
*
* The source column is declared NULL in the CREATE TABLE DDL so dbDelta can add it
* to existing tables without a DEFAULT. PHP always writes the value explicitly on
* insert, but rows created before 1.0.9 need a one-time backfill.
*
* @since x.x.x
* @param string $table_name The full table name including prefix.
* @return void
*/
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'
)
);
}
Comment on lines +146 to +156

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.


/**
* Grant plugin capabilities to their default roles.
*
* Called during the 1.0.9 migration so that users who update the plugin
* (rather than deactivate and reactivate) also receive the capabilities.
* Uses edac_get_plugin_capabilities() so that the pro plugin's caps — registered
* via the edac_plugin_capabilities filter at plugins_loaded — are included when
* this migration runs on admin_init.
*
* @since x.x.x
* @return void
*/
private function grant_plugin_capabilities(): void {
foreach ( edac_get_plugin_capabilities() as $cap => $roles ) {
foreach ( $roles as $role_name ) {
$role = get_role( $role_name );
if ( $role ) {
$role->add_cap( $cap );
}
}
}
}

/**
* Migrate existing records to use selector-based unique identifiers.
*
Expand Down
10 changes: 10 additions & 0 deletions includes/activation.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,14 @@ function edac_activation() {
// Set transient to trigger redirect to welcome page.
// This will be checked on admin_init and deleted after redirect.
set_transient( 'edac_activation_redirect', true, 60 );

// Grant plugin capabilities to their default roles.
foreach ( edac_get_plugin_capabilities() as $cap => $roles ) {
foreach ( $roles as $role_name ) {
$role = get_role( $role_name );
if ( $role ) {
$role->add_cap( $cap );
}
}
}
}
21 changes: 16 additions & 5 deletions includes/classes/class-enqueue-frontend.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,20 @@ public static function maybe_enqueue_frontend_highlighter() {


wp_enqueue_style( 'edac-frontend-highlighter-app', plugin_dir_url( EDAC_PLUGIN_FILE ) . 'build/css/frontendHighlighterApp.css', false, EDAC_VERSION, 'all' );
wp_enqueue_script( 'edac-frontend-highlighter-app', plugin_dir_url( EDAC_PLUGIN_FILE ) . 'build/frontendHighlighterApp.bundle.js', false, EDAC_VERSION, false );

wp_localize_script(
'edac-frontend-highlighter-app',
'edacFrontendHighlighterApp',
wp_enqueue_script( 'edac-frontend-highlighter-app', plugin_dir_url( EDAC_PLUGIN_FILE ) . 'build/frontendHighlighterApp.bundle.js', [ 'wp-hooks' ], EDAC_VERSION, false );

/**
* Filter the data passed to the frontend highlighter JavaScript app.
*
* Pro plugin hooks in to add capability flags such as
* `canCreateManualIssues`, `canEditManualIssues`, and `canDeleteManualIssues`.
*
* @since x.x.x
*
* @param array $app_data The data array passed to wp_localize_script.
*/
$app_data = apply_filters(
'edac_frontend_highlighter_app_data',
[
'postID' => $post_id,
'nonce' => wp_create_nonce( 'frontend-highlighter' ),
Expand All @@ -169,6 +178,8 @@ public static function maybe_enqueue_frontend_highlighter() {
]
);

wp_localize_script( 'edac-frontend-highlighter-app', 'edacFrontendHighlighterApp', $app_data );

wp_set_script_translations( 'edac-frontend-highlighter-app', 'accessibility-checker', plugin_dir_path( EDAC_PLUGIN_FILE ) . 'languages' );

}
Expand Down
4 changes: 2 additions & 2 deletions includes/classes/class-summary-generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ private function count_errors() {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for interacting with custom database, safe variable used for table name, caching not required for one time operation.
$errors_count = $wpdb->get_var(
$wpdb->prepare(
'SELECT count(*) FROM %i where siteid = %d and postid = %d and ruletype = %s and ignre = %d',
"SELECT count(*) FROM %i where siteid = %d and postid = %d and ruletype = %s and ignre = %d and (source = 'automated' OR source IS NULL)",
$wpdb->prefix . 'accessibility_checker',
$this->site_id,
$this->post_id,
Expand All @@ -165,7 +165,7 @@ private function count_warnings() {
global $wpdb;

$warnings_parameters = [ get_current_blog_id(), $this->post_id, 'warning', 0 ];
$warnings_where = 'WHERE siteid = %d and postid = %d and ruletype = %s and ignre = %d';
$warnings_where = "WHERE siteid = %d and postid = %d and ruletype = %s and ignre = %d and (source = 'automated' OR source IS NULL)";
if ( defined( 'ANWW_VERSION' ) ) {
array_push( $warnings_parameters, 'link_blank' );
$warnings_where .= ' and rule != %s';
Expand Down
10 changes: 10 additions & 0 deletions includes/deactivation.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,14 @@ function edac_deactivation() {

// Unschedule the daily license check cron event.
wp_clear_scheduled_hook( 'edac_check_license_hook' );

// Remove plugin capabilities added on activation.
foreach ( edac_get_plugin_capabilities() as $cap => $roles ) {
foreach ( $roles as $role_name ) {
$role = get_role( $role_name );
if ( $role ) {
$role->remove_cap( $cap );
}
}
}
}
29 changes: 27 additions & 2 deletions includes/helper-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -808,8 +808,8 @@ function edac_remove_corrected_posts( $post_ID, $type, $pre = 1, $ruleset = 'php
}

$sql = 1 === $pre
? "UPDATE {$wpdb->prefix}accessibility_checker SET recordcheck = %d WHERE siteid = %d AND postid = %d AND type = %s"
: "DELETE FROM {$wpdb->prefix}accessibility_checker WHERE recordcheck = %d AND siteid = %d AND postid = %d AND type = %s";
? "UPDATE {$wpdb->prefix}accessibility_checker SET recordcheck = %d WHERE siteid = %d AND postid = %d AND type = %s AND (source = 'automated' OR source IS NULL)"
: "DELETE FROM {$wpdb->prefix}accessibility_checker WHERE recordcheck = %d AND siteid = %d AND postid = %d AND type = %s AND (source = 'automated' OR source IS NULL)";

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation.
$wpdb->query(
Expand Down Expand Up @@ -1177,3 +1177,28 @@ function edac_icon( string $name = 'check', string $type = '', bool $aria_hidden
. $svgs[ $name ]
. '</span>';
}

/**
* Returns the map of plugin capabilities to their default WordPress roles.
*
* Each key is a capability slug; each value is an array of role names that
* receive the capability on activation and lose it on deactivation.
*
* Filterable so the pro plugin (and any third-party add-on) can register
* additional capabilities through the same activate/deactivate system
* without modifying the free plugin.
*
* @since x.x.x
*
* @return array<string, string[]> Capability slug => role names.
*/
function edac_get_plugin_capabilities(): array {
/**
* Filters the plugin capability map used during activation and deactivation.
*
* @since x.x.x
*
* @param array<string, string[]> $caps Capability slug => array of role names.
*/
return apply_filters( 'edac_plugin_capabilities', [] );
}
50 changes: 49 additions & 1 deletion src/frontendHighlighterApp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { computePosition, autoUpdate } from '@floating-ui/dom';
import { createFocusTrap } from 'focus-trap';
import { isFocusable } from 'tabbable';
import { __, _n, sprintf } from '@wordpress/i18n';
import { doAction, applyFilters } from '@wordpress/hooks';
import { saveFixSettings } from '../common/saveFixSettingsRest';
import { fillFixesModal, fixSettingsModalInit, openFixesModal } from './fixesModal';
import { getLandmarkType as getLandmarkTypeUtil } from './getLandmarkType';
Expand Down Expand Up @@ -184,6 +185,17 @@ class AccessibilityCheckerHighlight {
// Docked panel restored on page load — fetch issue data so the panel isn't empty.
this.panelOpen();
}

/**
* Fires after the highlighter has finished initialising.
*
* Pro plugin uses this to register its manual-issues JS module.
*
* @since x.x.x
*
* @param {AccessibilityCheckerHighlight} highlighter The highlighter instance.
*/
doAction( 'edac.highlighter.init', this );
}

toggleMenu() {
Expand Down Expand Up @@ -641,6 +653,17 @@ class AccessibilityCheckerHighlight {
document.body.insertAdjacentHTML( 'afterbegin', newElement );
const panel = document.getElementById( 'edac-highlight-panel' );

/**
* Fires after the highlighter menu is added to the DOM.
*
* Pro plugin uses this to append additional menu items such as "Add Manual Issue".
*
* @since x.x.x
*
* @param {HTMLElement} menuEl The `<ul role="menu">` element.
*/
doAction( 'edac.highlighter.menuItems', document.getElementById( 'edac-highlight-menu' ) );

// Override --wp-admin-theme-color with the correct value from the user's
// admin color scheme, since WordPress does not update this variable on the frontend.
if ( edacFrontendHighlighterApp?.adminThemeColor ) {
Expand Down Expand Up @@ -1348,6 +1371,18 @@ class AccessibilityCheckerHighlight {
}
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 x.x.x
*
* @param {Object} issue The issue data object.
* @param {HTMLElement} contentEl The `.edac-highlight-panel-controls-content-issue` element.
*/
doAction( 'edac.highlighter.issueDetail', matchingObj, issueContent );
}
}
Comment on lines 1372 to 1387

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.

}
Expand Down Expand Up @@ -1625,7 +1660,20 @@ class AccessibilityCheckerHighlight {
) );
}

div.innerHTML = `<span class="edac-highlight-summary-total" role="heading" aria-level="3">${ totalLabel }</span><span class="edac-highlight-summary-breakdown">${ breakdownParts.join( ' · ' ) }</span>`;
/**
* Filters the summary breakdown parts in the highlighter panel footer.
*
* Pro plugin uses this to append a "X Manual" count alongside the
* Problems / Needs Review / Dismissed breakdown.
*
* @since x.x.x
*
* @param {string[]} parts Summary parts array.
* @param {Object} counts Raw counts: errorCount, warningCount, ignoredCount.
*/
const summaryParts = applyFilters( 'edac.highlighter.issueGroups', breakdownParts, { errorCount, warningCount, ignoredCount } );

div.innerHTML = `<span class="edac-highlight-summary-total" role="heading" aria-level="3">${ totalLabel }</span><span class="edac-highlight-summary-breakdown">${ summaryParts.join( ' · ' ) }</span>`;
}

/**
Expand Down
1 change: 1 addition & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,6 @@ module.exports = {
'@wordpress/html-entities': [ 'wp', 'htmlEntities' ],
'@wordpress/code-editor': [ 'wp', 'codeEditor' ],
'@wordpress/a11y': [ 'wp', 'a11y' ],
'@wordpress/hooks': [ 'wp', 'hooks' ],
},
};
Loading