Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix implicit attribute use in duplicate-id #374

Merged
merged 12 commits into from
Jul 13, 2017
Merged

Conversation

WilcoFiers
Copy link
Contributor

The id property can be overloaded, so we should use getAttribute()

@WilcoFiers WilcoFiers changed the title fix implicit attribute us in duplicate-id fix implicit attribute use in duplicate-id Jun 20, 2017
@dequelabs dequelabs deleted a comment from WilcoFiers Jun 21, 2017
@dylanb
Copy link
Contributor

dylanb commented Jun 21, 2017

ok, I deleted the confusing comments and replies so I could clarify:

  1. This only happens on form elements
  2. When it happens, the id attribute changes from a string to a reference to an HTMLElement
  3. There are 39 places in the code where we refer to the element.id - this changes only one of them

I am not sure whether this is documented browser behavior or a side-effect? Can you find a reference?

If it is documented, we might be better off detecting it and having a rule that raises this as a violation rather than trying to fix this everywhere.

if (element.getAttribute('id')) {
o = document.querySelector(
'[aria-owns~=' +
axe.commons.utils.escapeSelector(element.getAttribute('id')) +
Copy link
Contributor

@marcysutton marcysutton Jun 24, 2017

Choose a reason for hiding this comment

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

The line breaks don't make this much easier to read. Perhaps the escapeSelector line should be indented to indicate a group?

@WilcoFiers
Copy link
Contributor Author

@dylanb @marcysutton Any further comments?

@marcysutton
Copy link
Contributor

Looks good to me!

@dylanb
Copy link
Contributor

dylanb commented Jun 30, 2017 via email

@WilcoFiers
Copy link
Contributor Author

@dylanb Don't merge, because ... ?

@@ -4,7 +4,11 @@ this.data({
});

var matchingNodes = document.querySelectorAll('input[type="' +
axe.commons.utils.escapeSelector(node.type) + '"][name="' + axe.commons.utils.escapeSelector(node.name) + '"]');
axe.commons.utils.escapeSelector(node.getAttribute('type')) +
Copy link
Contributor

Choose a reason for hiding this comment

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

Why these changes?

@@ -1,3 +1,5 @@
var title = axe.commons.text.sanitize(node.title).trim().toLowerCase();
var title = axe.commons.text.sanitize(
Copy link
Contributor

Choose a reason for hiding this comment

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

Why?

@@ -75,7 +75,7 @@ dom.isInTextBlock = function isInTextBlock(node) {
return false;

// Don't walk links, we're only interested in what's not in them.
} else if ((nodeName === 'A' && currNode.href) ||
} else if ((nodeName === 'A' && currNode.hasAttribute('href')) ||
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not the same - don't change this

@@ -32,21 +33,23 @@ function findLabel(element) {
}

function isButton(element) {
return ['button', 'reset', 'submit'].indexOf(element.type) !== -1;
return ['button', 'reset', 'submit'].includes(
(element.getAttribute('type') || '').toLowerCase()
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't change this

}

function isInput(element) {
var nodeName = element.nodeName.toUpperCase();
return (nodeName === 'TEXTAREA' || nodeName === 'SELECT') ||
(nodeName === 'INPUT' && element.type.toLowerCase() !== 'hidden');
(nodeName === 'INPUT' && (element.getAttribute('type') || '').toLowerCase() !== 'hidden');
Copy link
Contributor

Choose a reason for hiding this comment

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

don't change this

default:
return false;
}
}

function shouldCheckAlt(element) {
var nodeName = element.nodeName.toUpperCase();
return (nodeName === 'INPUT' && element.type.toLowerCase() === 'image') ||
['IMG', 'APPLET', 'AREA'].indexOf(nodeName) !== -1;
return (nodeName === 'INPUT' && (element.getAttribute('type') || '').toLowerCase() === 'image') ||
Copy link
Contributor

Choose a reason for hiding this comment

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

don't change this

@@ -199,7 +202,12 @@ text.accessibleText = function(element, inLabelledByContext) {

if (isInput(element) && !inControlContext) {
if (isButton(element)) {
return element.value || element.title || defaultButtonValues[element.type] || '';
return (
Copy link
Contributor

Choose a reason for hiding this comment

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

don't change

@@ -81,15 +81,15 @@ const createSelector = {
nodeName = escapeSelector(nodeName);
// Add [type] if nodeName is an input element
if (nodeName === 'input' && elm.hasAttribute('type')) {
nodeName += '[type="' + elm.type + '"]';
nodeName += `[type="${ elm.getAttribute('type') }"]`;
Copy link
Contributor

Choose a reason for hiding this comment

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

don't change

}
return nodeName;
}
},
// Has a name property, but no ID (Think input fields)
getElmNameProp (elm) {
if (!elm.id && elm.name) {
return '[name="' + escapeSelector(elm.name) + '"]';
if (!elm.getAttribute('id') && elm.getAttribute('name')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

don't change

@@ -1,7 +1,7 @@
/* global document */

var nodeName = node.nodeName.toUpperCase(),
nodeType = node.type,
nodeType = node.getAttribute('type'),
Copy link
Contributor

Choose a reason for hiding this comment

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

don't change

@@ -13,7 +13,7 @@ if (nodeName === 'INPUT') {
}

if (nodeName === 'SELECT') {
return !!node.options.length && !node.disabled;
return !!node.options.length && !node.hasAttribute('disabled');
Copy link
Contributor

Choose a reason for hiding this comment

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

don't change

@@ -24,11 +24,11 @@ if (nodeName === 'OPTION') {
return false;
}

if (nodeName === 'BUTTON' && node.disabled || axe.commons.dom.findUp(node, 'button[disabled]')) {
if (nodeName === 'BUTTON' && node.hasAttribute('disabled') || axe.commons.dom.findUp(node, 'button[disabled]')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

don't change

return false;
}

if (nodeName === 'FIELDSET' && node.disabled || axe.commons.dom.findUp(node, 'fieldset[disabled]')) {
if (nodeName === 'FIELDSET' && node.hasAttribute('disabled') || axe.commons.dom.findUp(node, 'fieldset[disabled]')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

don't change

@@ -40,23 +40,25 @@ if (nodeName === 'LABEL' || nodeParentLabel) {
relevantNode = nodeParentLabel;
}
// explicit label of disabled input
var candidate = relevantNode.htmlFor && doc.getElementById(relevantNode.htmlFor);
if (candidate && candidate.disabled) {
var candidate = relevantNode.hasAttribute('for') && doc.getElementById(relevantNode.getAttribute('for'));
Copy link
Contributor

Choose a reason for hiding this comment

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

don't change

@dylanb
Copy link
Contributor

dylanb commented Jul 2, 2017

@WilcoFiers because I had not had time to review it yet...now that I have, I am totally confused - I thought that this change was specifically about the id attribute getting overridden within a <form> element - why did you change a bunch of other attributes?

@WilcoFiers
Copy link
Contributor Author

@dylanb The problem isn't limited to IDs. From what I found, any property on html, body, and form elements can be replaced with an accessor to a different element. You can even overwrite things like getAttribute and parentNode if you want. This stuff is part of DOM 0, but all browsers I tested still support it: https://openhome.cc/eGossip/JavaScript/Level0DOM.html

It's fortunately limited to a few elements, but because we're often using role selectors, we can't ever be entirely sure that element properties aren't what we think. You could even replace things like parentNode if you wanted. Some of these are less likely to be used then others as element names. I think we should avoid accessing properties that are likely to be used as field names, things like name, title, id, type and disabled (as in having a disability).

Probably .htmlFor and .href are secure. I could remove those, but I would recommend we keep the rest in. I could do some performance tests if you want, I'm sure there's a cost to this.

@dylanb
Copy link
Contributor

dylanb commented Jul 3, 2017

@WilcoFiers have we had complaints from users that this is occurring? I found 39 uses of simply .id within the code base. The changes you have made so far address only a fraction of even these cases. So if we are going to make these changes, we need to make them everywhere.

That being said, I don't see the need to make these changes if we have not had complaints because that means that this bad use of HTML is not occurring frequently enough to warrant the required changes.

Also, not all attributes return the same value as the call to getAttribute() - href is one example. This means that we need to not simply replace the uses, we need to inspect the code for each use and make sure that the implementation is not relying on those differences.

Bottom line: I don't think this is worth it.

@WilcoFiers WilcoFiers force-pushed the fix/implicit-id-attr branch from 428eea2 to a2b73f2 Compare July 4, 2017 11:43
@WilcoFiers
Copy link
Contributor Author

WilcoFiers commented Jul 4, 2017

@dylanb fair enough. I'll roll some stuff back. As for replacing all instances of .id, most of them come from rules and checks, not element references. I went through all of them.

* chore: convert spaces to tabs in Gruntfile

Align the file with the EditorConfig setting

* chore: ignore package-lock.json

This file is produced by NPM 5, but this repo didn't use the old
shrinkwrap version so this should be excluded
var label = document.querySelector('label[for="' + axe.commons.utils.escapeSelector(node.id) + '"]');
if (node.getAttribute('id')) {
const id = axe.commons.utils.escapeSelector(node.getAttribute('id'));
const label = document.querySelector(`label[for="${id}"]`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Some mixed indentation here.

axe.commons.utils.escapeSelector(node.id) + '"]')),
parent = node.parentNode;
const id = axe.commons.utils.escapeSelector(node.getAttribute('id'));
let labels = Array.from(document.querySelectorAll(`label[for="${id}"]`));
Copy link
Contributor

Choose a reason for hiding this comment

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

Array.from here is so slick. I love it!

@@ -30,7 +30,10 @@ var tableGrid = tableUtils.toGrid(node);

// Look for all the bad headers
var out = headers.reduce(function (res, header) {
if (header.id && reffedHeaders.indexOf(header.id) !== -1) {
if (
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this code fit on one line under 80 characters? It would be more consistent with our existing style.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree - lets keep our coding style consistent

if (cell.id) {
return !!document.querySelector('[headers~="' + axe.utils.escapeSelector(cell.id) + '"]');
if (cell.getAttribute('id')) {
const id = axe.utils.escapeSelector(cell.getAttribute('id'));
Copy link
Contributor

Choose a reason for hiding this comment

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

Indentation is mixed up here due to spaces/tabs, I'm guessing.

if(node.getAttribute && node.getAttribute('id') &&
node.ownerDocument.querySelectorAll('#' + axe.utils.escapeSelector(node.id)).length === 1) {

if(
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we put the first part of the expression inline with the if( for consistency's sake?

@@ -49,4 +49,11 @@ describe('duplicate-id', function () {
assert.isTrue(checks['duplicate-id'].evaluate.call(checkContext, node));
});

it('should allow overwrote ids', function () {
fixture.innerHTML = '<form data-testelm="1" id="target"><input name="id"></form>';
Copy link
Contributor

Choose a reason for hiding this comment

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

This might be overkill for test code...but as stewards of accessibility it would be appropriate to have a label in the fixture.

@marcysutton
Copy link
Contributor

I left a few style comments, but otherwise this PR looks good to me.

@dylanb
Copy link
Contributor

dylanb commented Jul 13, 2017

@WilcoFiers only one remaining jshint problem, then we can merge

@WilcoFiers WilcoFiers merged commit 353b53f into develop Jul 13, 2017
@marcysutton marcysutton deleted the fix/implicit-id-attr branch July 19, 2017 19:42
mrtnvh pushed a commit to mrtnvh/axe-core that referenced this pull request Nov 24, 2023
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.

4 participants