Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.4' );
define( 'EDAC_DB_VERSION', '1.0.5' );
}

// Plugin Folder Path.
Expand Down
16 changes: 11 additions & 5 deletions admin/class-ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -739,20 +739,26 @@ function ( $value ) {
$ignre_comment = ( 'enable' === $action && isset( $_REQUEST['comment'] ) ) ? sanitize_textarea_field( wp_unslash( $_REQUEST['comment'] ) ) : null;
$ignore_global = ( 'enable' === $action && isset( $_REQUEST['ignore_global'] ) ) ? sanitize_textarea_field( wp_unslash( $_REQUEST['ignore_global'] ) ) : 0;

// If largeBatch is set and 'true', we need to perform an update using the 'object'
// If largeBatch is set and 'true', we need to perform an update using the 'selector'
// instead of IDs. It is a much less efficient query than by IDs - but many IDs run
// into request size limits which caused this to not function at all.
// We use selector as the unique identifier instead of object to properly handle
// duplicate code objects in different locations.
if ( isset( $_REQUEST['largeBatch'] ) && 'true' === $_REQUEST['largeBatch'] ) {
// Get the 'object' from the first id.
// Get the 'selector' and 'rule' from the first id.
$first_id = $ids[0];
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- We need to get the latest value, not a cached value.
$object = $wpdb->get_var( $wpdb->prepare( 'SELECT object FROM %i WHERE id = %d', $table_name, $first_id ) );
$row = $wpdb->get_row( $wpdb->prepare( 'SELECT selector, rule FROM %i WHERE id = %d', $table_name, $first_id ), ARRAY_A );

if ( ! $object ) {
if ( ! $row || ! $row['selector'] ) {
wp_send_json_error( new \WP_Error( '-2', __( 'No ignore data to return', 'accessibility-checker' ) ) );
}

$selector = $row['selector'];
$rule = $row['rule'];

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe variable used for table name, caching not required for one time operation.
$wpdb->query( $wpdb->prepare( 'UPDATE %i SET ignre = %d, ignre_user = %d, ignre_date = %s, ignre_comment = %s, ignre_global = %d WHERE siteid = %d and object = %s', $table_name, $ignre, $ignre_user, $ignre_date, $ignre_comment, $ignore_global, $siteid, $object ) );
$wpdb->query( $wpdb->prepare( 'UPDATE %i SET ignre = %d, ignre_user = %d, ignre_date = %s, ignre_comment = %s, ignre_global = %d WHERE siteid = %d and selector = %s and rule = %s', $table_name, $ignre, $ignre_user, $ignre_date, $ignre_comment, $ignore_global, $siteid, $selector, $rule ) );
Comment thread
SteveJonesDev marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
} else {
// For small batches of IDs, we can just loop through.
foreach ( $ids as $id ) {
Expand Down
21 changes: 16 additions & 5 deletions admin/class-insert-rule-data.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
/**
* Class for inserting rule data into the database
*
* The unique identifier for issues changed in version 1.0.5 of the database schema.
* Previously, issues were identified by: postid + rule + object + type + siteid
* Now, issues are identified by: postid + rule + selector + type + siteid
*
* This change allows duplicate code objects (e.g., two empty paragraphs) to be
* stored as separate issues when they appear in different locations on the page.
* The selector field provides the unique location identifier for each issue.
*
* @since 1.10.0
*/
class Insert_Rule_Data {
Expand Down Expand Up @@ -73,15 +81,17 @@ public function insert( object $post, string $rule, string $ruletype, string $ru
}

// Check if exists.
// Use selector as the unique identifier instead of object to allow duplicate code objects
// with different selectors (e.g., two empty paragraphs in different locations).
// 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 object = %s and siteid = %d',
'SELECT postid, ignre FROM %i where type = %s and postid = %d and rule = %s and selector = %s and siteid = %d',
$table_name,
$rule_data['type'],
$rule_data['postid'],
$rule_data['rule'],
$rule_data['object'],
$rule_data['selector'],
$rule_data['siteid']
),
ARRAY_A
Expand All @@ -97,22 +107,23 @@ public function insert( object $post, string $rule, string $ruletype, string $ru
}

// update existing record.
// Use selector for WHERE clause instead of object to match on unique identifier.
// 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(
$wpdb->prepare(
'UPDATE %i SET recordcheck = %d, landmark = %s, landmark_selector = %s, selector = %s, ancestry = %s, xpath = %s, ignre = %d WHERE siteid = %d and postid = %d and rule = %s and object = %s and type = %s',
'UPDATE %i SET recordcheck = %d, landmark = %s, landmark_selector = %s, object = %s, ancestry = %s, xpath = %s, ignre = %d WHERE siteid = %d and postid = %d and rule = %s and selector = %s and type = %s',
$table_name,
1,
$rule_data['landmark'],
$rule_data['landmark_selector'],
$rule_data['selector'],
$rule_data['object'],
$rule_data['ancestry'],
$rule_data['xpath'],
$rule_data['ignre'],
$rule_data['siteid'],
$rule_data['postid'],
$rule_data['rule'],
$rule_data['object'],
$rule_data['selector'],
$rule_data['type']
)
);
Expand Down
29 changes: 29 additions & 0 deletions admin/class-update-database.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,38 @@ public function edac_update_database() {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );

// Run migration for selector-based unique identifiers if upgrading from older versions.
if ( version_compare( $db_version, '1.0.5', '<' ) ) {
$this->migrate_to_selector_based_unique_id();
}
}

// Update database version option.
update_option( 'edac_db_version', sanitize_text_field( EDAC_DB_VERSION ) );
}

/**
* Migrate existing records to use selector-based unique identifiers.
*
* This migration handles records that were created before the selector field
* was used as the unique identifier. Records with NULL selectors will have
* a fallback identifier generated based on their ID to ensure uniqueness.
*
* @since 1.0.5
* @return void
*/
private function migrate_to_selector_based_unique_id() {
global $wpdb;
$table_name = $wpdb->prefix . 'accessibility_checker';

// Find records with NULL or empty selectors and update them with a fallback value.
// Using the record ID ensures each record has a unique selector for backward compatibility.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time migration query.
$wpdb->query(
$wpdb->prepare(
"UPDATE %i SET selector = CONCAT('legacy-id-', id) WHERE selector IS NULL OR selector = ''",
$table_name
)
);
}
}
47 changes: 47 additions & 0 deletions tests/phpunit/Admin/InsertRuleDataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,51 @@ public function testRuleInserterReturnLogic() {
$current_row_count = $wpdb->get_var( "SELECT COUNT(*) FROM $this->table_name" ); // phpcs:ignore WordPress.DB -- caching not required for one time operation.
$this->assertEquals( $initial_row_count + 1, $current_row_count );
}

/**
* Tests that duplicate objects with different selectors are stored separately.
* This test verifies the fix for the issue where duplicate code objects
* (like two empty paragraphs) were only flagged as one issue.
*/
public function testDuplicateObjectsWithDifferentSelectors() {
$post = $this->factory()->post->create_and_get();
$rule = 'empty_paragraph_tag';
$ruletype = 'warning';
$rule_obj = '<p></p>';

global $wpdb;

$rule_inserter = new Insert_Rule_Data();
$initial_row_count = $wpdb->get_var( "SELECT COUNT(*) FROM $this->table_name" ); // phpcs:ignore WordPress.DB -- caching not required for one time operation.

// Insert first empty paragraph with selector 1.
$selectors_1 = [
'selector' => [ 'div.content > p:nth-child(1)' ],
'ancestry' => [ 'div.content', 'p' ],
'xpath' => [ '/html/body/div/p[1]' ],
];
$result_1 = $rule_inserter->insert( $post, $rule, $ruletype, $rule_obj, null, null, $selectors_1 );
$this->assertIsInt( $result_1 );

// Insert second empty paragraph with different selector - should be stored as separate issue.
$selectors_2 = [
'selector' => [ 'div.content > p:nth-child(5)' ],
'ancestry' => [ 'div.content', 'p' ],
'xpath' => [ '/html/body/div/p[5]' ],
];
$result_2 = $rule_inserter->insert( $post, $rule, $ruletype, $rule_obj, null, null, $selectors_2 );
$this->assertIsInt( $result_2 );

// Verify two separate records were created.
$current_row_count = $wpdb->get_var( "SELECT COUNT(*) FROM $this->table_name" ); // phpcs:ignore WordPress.DB -- caching not required for one time operation.
$this->assertEquals( $initial_row_count + 2, $current_row_count );

// Verify inserting the same object with same selector is treated as duplicate.
$result_3 = $rule_inserter->insert( $post, $rule, $ruletype, $rule_obj, null, null, $selectors_1 );
$this->assertEquals( null, $result_3 );

// Verify row count hasn't changed.
$final_row_count = $wpdb->get_var( "SELECT COUNT(*) FROM $this->table_name" ); // phpcs:ignore WordPress.DB -- caching not required for one time operation.
$this->assertEquals( $initial_row_count + 2, $final_row_count );
}
}
Loading