





















































In this article by Ivo Balbaert author of Dart Cookbook, we will cover the following recipes:
(For more resources related to this topic, see here.)
We've all heard of (or perhaps even experienced) cross-site scripting (XSS) attacks, where evil minded attackers try to inject client-side script or SQL statements into web pages. This could be done to gain access to session cookies or database data, or to get elevated access-privileges to sensitive page content. To verify an HTML document and produce a new HTML document that preserves only whatever tags are designated safe is called sanitizing the HTML.
Look at the web project sanitization. Run the following script and see how the text content and default sanitization works:
var elem1 = new Element.html('<div class="foo">content</div>'); document.body.children.add(elem1); var elem2 = new Element.html('<script class="foo">evil content</script><p>ok?</p>'); document.body.children.add(elem2);
The text content and ok? from elem1 and elem2 are displayed, but the console gives the message Removing disallowed element <SCRIPT>. So a script is removed before it can do harm.
import 'dart:convert' show HtmlEscape;
In main(), use the following code:
var unsafe = '<script class="foo">evil content</script><p>ok?</p>'; var sanitizer = const HtmlEscape(); print(sanitizer.convert(unsafe));
This prints the following output to the console:
<script class="foo">evil content</script><p>ok?</p>
var html_string = '<p class="note">a note aside</p>'; var node1 = new Element.html( html_string, validator: new NodeValidatorBuilder() ..allowElement('a', attributes: ['href']) );
The console prints the following output:
Removing disallowed element <p> Breaking on exception: Bad state: No elements
final allHtml = const NullTreeSanitizer(); class NullTreeSanitizer implements NodeTreeSanitizer { const NullTreeSanitizer(); void sanitizeTree(Node node) {} }
It can also be used as follows:
var elem3 = new Element.html('<p>a text</p>'); elem3.setInnerHtml(html_string, treeSanitizer: allHtml);
First, we have very good news: Dart automatically sanitizes all methods through which HTML elements are constructed, such as new Element.html(), Element.innerHtml(), and a few others. With them, you can build HTML hardcoded, but also through string interpolation, which entails more risks. The default sanitization removes all scriptable elements and attributes.
If you want to escape all characters in a string so that they are transformed into HTML special characters (such as ;/ for a /), use the class HTMLEscape from dart:convert as shown in the second step. The default behavior is to escape apostrophes, greater than/less than, quotes, and slashes. If your application is using untrusted HTML to put in variables, it is strongly advised to use a validation scheme, which only covers the syntax you expect users to feed into your app. This is possible because Element.html() has the following optional arguments:
Element.html(String html, {NodeValidator validator, NodeTreeSanitizer treeSanitizer})
In step 3, only <a> was an allowed tag. By adding more allowElement rules in cascade, you can allow more tags. Using allowHtml5() permits all HTML5 tags.
If you want to remove all control in some cases (perhaps you are dealing with known safe HTML and need to bypass sanitization for performance reasons), you can add the class NullTreeSanitizer to your code, which has no control at all and defines an object allHtml, as shown in step 4. Then, use setInnerHtml() with an optional named attribute treeSanitizer set to allHtml.
Local storage (also called the Web Storage API) is widely supported in modern browsers. It enables the application's data to be persisted locally (on the client side) as a map-like structure: a dictionary of key-value string pairs, in fact using JSON strings to store and retrieve data. It provides our application with an offline mode of functioning when the server is not available to store the data in a database. Local storage does not expire, but every application can only access its own data up to a certain limit depending on the browser. In addition, of course, different browsers can't access each other's stores.
Look at the following example, the local_storage.dart file:
import 'dart:html'; Storage local = window.localStorage; void main() { var job1 = new Job(1, "Web Developer", 6500, "Dart Unlimited") ;
Perform the following steps to use the browser's local storage:
local["Job:${job1.id}"] = job1.toJson; ButtonElement bel = querySelector('#readls'); bel.onClick.listen(readShowData); }
readShowData(Event e) { var key = 'Job:1'; if(local.containsKey(key)) { // read data from local storage: String job = local[key]; querySelector('#data').appendText(job); } } class Job { int id; String type; int salary; String company; Job(this.id, this.type, this.salary, this.company); String get toJson => '{ "type": "$type", "salary": "$salary", "company": "$company" } '; }
The following screenshot depicts how data is stored in and retrieved from a local storage:
You can store data with a certain key in the local storage from the Window class as follows using window.localStorage[key] = data; (both key and data are Strings).
You can retrieve it with var data = window.localStorage[key];.
In our code, we used the abbreviation Storage local = window.localStorage; because local is a map. You can check the existence of this piece of data in the local storage with containsKey(key); in Chrome (also in other browsers via Developer Tools). You can verify this by navigating to Extra | Tools | Resources | Local Storage (as shown in the previous screenshot), window.localStorage also has a length property; you can query whether it contains something with isEmpty, and you can loop through all stored values using the following code:
for(var key in window.localStorage.keys) { String value = window.localStorage[key]; // more code }
Local storage can be disabled (by user action, or via an installed plugin or extension), so we must alert the user when this needs to be enabled; we can do this by catching the exception that occurs in this case:
try { window.localStorage[key] = data; } on Exception catch (ex) { window.alert("Data not stored: Local storage is disabled!"); }
Local storage is a simple key-value store and does have good cross-browser coverage. However, it can only store strings and is a blocking (synchronous) API; this means that it can temporarily pause your web page from responding while it is doing its job storing or reading large amounts of data such as images. Moreover, it has a space limit of 5 MB (this varies with browsers); you can't detect when you are nearing this limit and you can't ask for more space. When the limit is reached, an error occurs so that the user can be informed.
These properties make local storage only useful as a temporary data storage tool; this means that it is better than cookies, but not suited for a reliable, database kind of storage.
Web storage also has another way of storing data called sessionStorage used in the same way, but this limits the persistence of the data to only the current browser session. So, data is lost when the browser is closed or another application is started in the same browser window.
When, for some reason, our users don't have web access or the website is down for maintenance (or even broken), our web-based applications should also work offline. The browser cache is not robust enough to be able to do this, so HTML5 has given us the mechanism of ApplicationCache. This cache tells the browser which files should be made available offline. The effect is that the application loads and works correctly, even when the user is offline. The files to be held in the cache are specified in a manifest file, which has a .mf or .appcache extension.
Look at the appcache application; it has a manifest file called appcache.mf.
<html manifest="appcache.mf">
If a page has to be cached and doesn't have the manifest attribute, it must be specified in the CACHE section of the manifest file. The manifest file has the following (minimum) content:
CACHE MANIFEST # 2012-09-28:v3 CACHE: Cached1.html appcache.css appcache.dart http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js NETWORK: * FALLBACK: / offline.html
main() { new AppCache(window.applicationCache); } class AppCache { ApplicationCache appCache; AppCache(this.appCache) { appCache.onUpdateReady.listen((e) => updateReady()); appCache.onError.listen(onCacheError); } void updateReady() { if (appCache.status == ApplicationCache.UPDATEREADY) { // The browser downloaded a new app cache. Alert the user: appCache.swapCache(); window.alert('A new version of this site is available. Please reload.'); } } void onCacheError(Event e) { print('Cache error: ${e}'); // Implement more complete error reporting to developers } }
The CACHE section in the manifest file enumerates all the entries that have to be cached. The NETWORK: and * options mean that to use all other resources the user has to be online. FALLBACK specifies that offline.html will be displayed if the user is offline and a resource is inaccessible. A page is cached when either of the following is true:
The browser is notified when the manifest file is changed, and the user will be forced to refresh its cached resources. Adding a timestamp and/or a version number such as # 2014-05-18:v1 works fine. Changing the date or the version invalidates the cache, and the updated pages are again loaded from the server.
To access the browser's app cache from your code, use the window.applicationCache object. Make an object of a class AppCache, and alert the user when the application cache has become invalid (the status is UPDATEREADY) by defining an onUpdateReady listener.
The other known states of the application cache are UNCACHED, IDLE, CHECKING, DOWNLOADING, and OBSOLETE. To log all these cache events, you could add the following listeners to the appCache constructor:
appCache.onCached.listen(onCacheEvent); appCache.onChecking.listen(onCacheEvent); appCache.onDownloading.listen(onCacheEvent); appCache.onNoUpdate.listen(onCacheEvent); appCache.onObsolete.listen(onCacheEvent); appCache.onProgress.listen(onCacheEvent);
Provide an onCacheEvent handler using the following code:
void onCacheEvent(Event e) { print('Cache event: ${e}'); }
The default action for a submit button on a web page that contains an HTML form is to post all the form data to the server on which the application runs. What if we don't want this to happen?
Experiment with the submit application by performing the following steps:
<form id="form1" action="http://www.dartlang.org" method="POST"> <label>Job:<input type="text" name="Job" size="75"></input> </label> <input type="submit" value="Job Search"> </form>
Comment out all the code in submit.dart. Run the app, enter a job name, and click on the Job Search submit button; the Dart site appears.
import 'dart:html'; void main() { querySelector('#form1').onSubmit.listen(submit); } submit(Event e) { e.preventDefault(); // code to be executed when button is clicked }
In the first step, when the submit button is pressed, the browser sees that the method is POST. This method collects the data and names from the input fields and sends it to the URL specified in action to be executed, which only shows the Dart site in our case.
To prevent the form from posting the data, make an event handler for the onSubmit event of the form. In this handler code, e.preventDefault(); as the first statement will cancel the default submit action. However, the rest of the submit event handler (and even the same handler of a parent control, should there be one) is still executed on the client side.
In this article we learned how to handle web applications, sanitize a HTML, use a browser's local storage, use application cache to work offline, and how to prevent an onSubmit event from reloading a page.
Further resources on this subject: