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, specify the name of the desired filter as the second argument to get()
:
klass.get('username', 'html'); // => "<b>joe</b>"
klass.get('username', 'url'); // => "%3Cb%3Ejoe%3C%2Fb%3E"
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.
To remove a previously added filter:
// Removes the "html" filter if it exists.
Y.Attribute.removeFilter('html');
When removeFilter()
is called with the name of a filter that doesn't exist, it should simply do nothing.
A new attribute config property named filter
would 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.
To improve performance, Attribute could cache filtered values internally, clearing the cached value whenever an attribute's raw value is updated. There may be dragons here.
Internally, get()
or its underyling implementation should take the following steps:
-
Let
attrName
be the value of the first argument toget()
. -
Let
filterName
be the value of the second argument toget()
. -
If
attrName
refers to a nonexistent attribute, returnundefined
. -
Let
rawValue
be the raw value of the attribute or sub-attribute named byattrName
, after passing through the attribute's getter function if one is set. -
If
filterName
isundefined
ornull
, then-
If a default filter has been configured for the attribute, then
-
Let
filterName
be the name of the attribute's default filter. -
If
filterName
refers to a nonexistant filter, returnundefined
. -
Execute the default filter function, passing
rawValue
as the only argument. -
Return the filter function's return value.
-
-
Otherwise, return
rawValue
.
-
-
Otherwise:
-
If
filterName
equals "raw", returnrawValue
. -
Otherwise, if
filterName
refers to a nonexistent filter, returnundefined
. -
Execute the filter function, passing
rawValue
as the only argument. -
Return the filter function's return value.
-
My initial impression is that this is somewhat similar to getter in ATTRS, but with the option of having the raw value, which is good :)
I don't have any problem with the functionality listed. It seems like a straightforward way to abstract away repeated uses of getters which should be generic anyway.
The get() implementation mentions (1.f) that the value would come from calling .toString(). I don't think that this is a fair assumption, even though it would cover most cases.
The filter idea could be more flexible using the raw value regardless of its type. Eg. I could abuse your filter to retrieve date parts if the filter received a date object from the model, maybe by constructing a date part filter to grab parts of a date object. I could call klass.get('date:day'); or something similar and receive only the day part.
The other question that comes from comparing filter to a genericised version of a getter, is whether this design fits into a setter equivalent (yeah, this is going out of scope now :)). The model has the ability to parse itself, but not at the individual value level. genericised versions of value parsers could also be useful (not at model.load, but for individual .sets, due to too much overhead generated by filtering a whole modellist). Again, a date example: klass.set('date:iso9660', d) for parsing or klass.set('date:day', 2) for an example of modifying part of a value. This kind of filter could take logic out of the application and into a basic reusable component.