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.
-
Ok, I've read through the proposal and the comments.
What it seems to me you are describing is similar to what exists today in DataTable as column formatters. In fact, it's probably more accurate to s/filter/formatter/g because "filter" suggests the removal of some part of the thing rather than reformatting it.
Registered formatters were in YUI 2, and are one of the todo items for 3.6.0, allowing
I think the idea is good, though it might complicate DT's use of formatters.
That said, I am very uncomfortable with the string parsing approach. Attribute is upstream of a lot of code, including most customer class implementations. Adding more work to get() will slow the library and implementation code down across the board. And, more recently relevant to DataTable, the new delimiter will decrease the usable characters in attribute names. This might break existing code. From 3.4.1 to 3.5.0, the move from Record to Model breaks implementations that use periods in their column keys because the Model instances think they are trying to retrieve subattributes. I don't want this to happen again.
Related aside: the implementation steps for get() are absent consideration of subattributes.
This also seems related to the notion of attribute types. This has come up a number of times in the past, and I am certainly in favor of it. The premise being that attributes can be configured with a type (sibling of value, setter, getter, etc), and other config settings would be applied based on that type.
The parallel for mosen's suggested setter filter is DataSchema's parsers, or the type config hinted above. Those are both static configurations as opposed to per-operation configurations which is the focus of this proposal.
So, I'm sticking with my original suggestion for getAs(name, formatter), though I think get(name, formatter) will be sufficient, since AttributeCore's implementation of get() only passes through one argument to _getAttr() anyway.