-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Attribute Value Filters
Note: This document is a work in progress. To comment on the latest living version of this proposal, visit https://gist.github.com/2025242. That gist is a living document, whereas this page represents the latest working draft derived from the gist.
With more and more application logic moving to the client, and with YUI becoming more popular on the server, it's increasingly important to design APIs that handle user input safely. Currently, YUI modules that store user input in attributes must do one of two things: either escape user strings before setting an attribute, or escape them manually before using them.
Escaping automatically before storing the value is safest, but also inconvenient if you sometimes need the unescaped value, since you must then store two versions (probably in two different attributes). This can lead to API clutter and confusion. Escaping manually before use avoids API clutter but increases the likelihood of mistakes, and also clutters up the codebase in general. It significantly increases the chances that another developer who is unaware of the need to escape the value will inadvertently introduce a security vulnerability.
Attribute should provide a consistent, pluggable API for retrieving a filtered string value. Internally, string attributes would be stored as raw strings, and would be filtered on demand using the specified filter when the attribute is retrieved. This avoids the need for module developers to write custom getter functions or store both filtered and unfiltered values, and allows for flexible and safe usage in a variety of scenarios.
Filters should be pluggable via a static API on Y.Attribute
. This will allow the escape
module to provide a set of default filters, and custom modules can provide their own filters to meet custom needs.
Setting a value continues to work the same as it does today:
klass.set('username', '<b>joe</b>'); // stores "<b>joe</b>" as the raw attr value
Getting a value also works the same:
klass.get('username'); // => "<b>joe</b>"
To get a filtered version of a value, append a colon to the attribute name, followed by the name of the desired filter:
klass.get('username:html'); // => "<b>joe</b>"
klass.get('username:url'); // => "%3Cb%3Ejoe%3C%2Fb%3E"
Internally, get()
or its underyling implemenation should take the following steps:
-
If the first argument to
get()
matches the regex/^([^:]+):([\w-]+)$/
, thena. Let
attrName
be the value of the first subpattern match.b. Let
filterName
be the value of the second subpattern match.c. If
attrName
refers to a nonexistent attribute, returnundefined
.d. If
filterName
refers to a nonexistent filter, returnundefined
.e. Let
rawValue
be the raw value of the attribute named byattrName
.f. Let
stringValue
be the result of callingrawValue.toString()
.g. Execute the filter function, passing
stringValue
as the only argument.h. Return the filter function's return value.
-
Else, treat the entire argument as the attribute name and return the raw value of the matching attribute, or
undefined
if it doesn't exist.
Filters are registered statically on Y.Attribute
. Once registered, a filter is available for use on any class instance that uses Attribute, even if it was instantiated before the filter was registered.
To register a filter:
// Registers a new "html" filter unless one already exists.
Y.Attribute.addFilter('html', Y.Escape.html);
// Arbitrary filter.
Y.Attribute.addFilter('disemvowel', function (value) {
return value.replace(/[aeiou]+/g, '');
});
Internally, Attribute should store the filter function in a static object hash, with the name as the key.
If addFilter()
is called with the name of a filter that already exists, it should log an error and refuse to overwrite the existing filter unless the third argument is truthy:
// Registers an "html" filter, overwriting any pre-existing filter.
Y.Attribute.addFilter('html', Y.Escape.html, true);
When get()
is called, Attribute should look up the named filter (if any) in the static hash of registered filters and call it, passing in the raw attribute value. The return value of the filter function should be returned from get()
.
To improve performance, Attribute could cache filtered values internally, clearing the cached value whenever an attribute's raw value is updated.
A new attribute config property named filter
could allow module developers to specify a default filter to be used for an attribute. For example, I could define an attribute that should always be filtered as HTML by default:
// ...
ATTRS: {
username: {
filter: 'html'
}
}
// ...
This would cause get('username')
to run the "html" filter. I could still specify another filter if desired, or get('username:raw')
to get the raw, unfiltered value.
2