Web UI: add sanitize() function to fn.js library.

Change-Id: I2d8fedf737dfaa86362b83edab57967888414088
(cherry picked from commit 0fe05d6c80d17c7dd19528ad63914767934416f0)
diff --git a/web/gui/src/main/webapp/app/fw/util/fn.js b/web/gui/src/main/webapp/app/fw/util/fn.js
index e3b9600..9706478 100644
--- a/web/gui/src/main/webapp/app/fw/util/fn.js
+++ b/web/gui/src/main/webapp/app/fw/util/fn.js
@@ -430,6 +430,61 @@
         return child;
     }
 
+    // -----------------------------------------------------------------
+    // The next section deals with sanitizing external strings destined
+    // to be loaded via a .html() function call.
+
+    var matcher = /<\/?([a-zA-Z0-9]+)*(.*?)\/?>/igm,
+        whitelist = ['b', 'i', 'p', 'em', 'strong'],
+        warnlist = ['script', 'style'];
+
+    // Returns true if the tag is in the warn list, (and is not an end-tag)
+    function inWarnList(tag) {
+        return (warnlist.indexOf(tag.name) !== -1 && tag.full.indexOf('/') === -1);
+    }
+
+    function analyze(html) {
+        html = String(html) || '';
+
+        var matches = [],
+            match;
+
+        // extract all tags
+        while ((match = matcher.exec(html)) !== null) {
+            matches.push({
+                full: match[0],
+                name: match[1]
+                // NOTE: ignoring attributes {match[2].split(' ')} for now
+            });
+        }
+
+        return matches;
+    }
+
+    function sanitize(html) {
+        html = String(html) || '';
+
+        var matches = analyze(html);
+
+        // do not allow script tags or style tags
+        html = html.replace(/<script(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/script>/gim, '');
+        html = html.replace(/<style(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/style>/gim, '');
+
+        // filter out all but whitelisted tag types
+        matches.forEach(function (tag) {
+            if (whitelist.indexOf(tag.name) === -1) {
+                html = html.replace(tag.full, '');
+                if (inWarnList(tag)) {
+                    $log.warn('Unsanitary HTML input -- ' + tag.full + ' detected!');
+                }
+            }
+        });
+
+        // TODO: consider encoding HTML entities, e.g. '&' -> '&amp;'
+
+        return html;
+    }
+
 
     angular.module('onosUtil')
         .factory('FnService',
@@ -469,7 +524,8 @@
                 removeFromTrie: removeFromTrie,
                 trieLookup: trieLookup,
                 classNames: classNames,
-                extend: extend
+                extend: extend,
+                sanitize: sanitize
             };
     }]);
 
diff --git a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
index 6f999a6..a7bda70 100644
--- a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
@@ -19,6 +19,7 @@
  */
 describe('factory: fw/util/fn.js', function() {
     var $window,
+        $log,
         fs,
         someFunction = function () {},
         someArray = [1, 2, 3],
@@ -38,13 +39,22 @@
         }
     };
 
+    var mockLog = {
+        debug: function () {},
+        info: function () {},
+        warn: function () {},
+        error: function () {}
+    };
+
     beforeEach(function () {
         module(function ($provide) {
             $provide.value('$window', mockWindow);
+            $provide.value('$log', mockLog);
         });
     });
 
-    beforeEach(inject(function (_$window_, FnService) {
+    beforeEach(inject(function (_$log_, _$window_, FnService) {
+        $log = _$log_;
         $window = _$window_;
         fs = FnService;
     }));
@@ -217,7 +227,7 @@
             'debugOn', 'debug',
             'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'sameObjProps', 'containsObj', 'cap',
             'eecode', 'noPx', 'noPxStyle', 'endsWith', 'addToTrie', 'removeFromTrie', 'trieLookup',
-            'classNames', 'extend'
+            'classNames', 'extend', 'sanitize'
         ])).toBeTruthy();
     });
 
@@ -425,5 +435,57 @@
         expect(fs.endsWith("barfood", "foo")).toBe(false);
     });
 
+    // === Tests for sanitize()
+    function chkSan(u, s) {
+        expect(fs.sanitize(u)).toEqual(s);
+    }
+    function chkGood(g) {
+        chkSan(g, g)
+    }
+    it('should return foo', function () {
+        chkGood('foo');
+    });
+    it('should retain < b > tags', function () {
+        chkGood('foo <b>bar</b> baz');
+    });
+    it('should retain < i > tags', function () {
+        chkGood('foo <i>bar</i> baz');
+    });
+    it('should retain < p > tags', function () {
+        chkGood('foo <p>bar</p> baz');
+    });
+    it('should retain < em > tags', function () {
+        chkGood('foo <em>bar</em> baz');
+    });
+    it('should retain < strong > tags', function () {
+        chkGood('foo <strong>bar</strong> baz');
+    });
+
+    it('should reject < a > tags', function () {
+        chkSan('test <a href="hah">something</a> this', 'test something this');
+    });
+
+    it('should log a warning for < script > tags', function () {
+        spyOn($log, 'warn');
+        chkSan('<script>alert("foo");</script>', '');
+        expect($log.warn).toHaveBeenCalledWith(
+            'Unsanitary HTML input -- <script> detected!'
+        );
+    });
+    it('should log a warning for < style > tags', function () {
+        spyOn($log, 'warn');
+        chkSan('<style> h1 {color:red;} </style>', '');
+        expect($log.warn).toHaveBeenCalledWith(
+            'Unsanitary HTML input -- <style> detected!'
+        );
+    });
+
+    it('should completely strip < script >, remove < a >, retain < i >', function () {
+        chkSan(
+            'Hey <i>this</i> is <script>alert("foo");</script> <a href="meh">cool</a>',
+            'Hey <i>this</i> is  cool'
+        );
+    });
+
 });