Implemented WebSockets for GUI2
Change-Id: I4776ce392b1e8e94ebee938cf7df22791a1e0b8f
diff --git a/web/gui2/src/main/webapp/app/fw/util/fn.service.ts b/web/gui2/src/main/webapp/app/fw/util/fn.service.ts
index 0726afc..d355ce9 100644
--- a/web/gui2/src/main/webapp/app/fw/util/fn.service.ts
+++ b/web/gui2/src/main/webapp/app/fw/util/fn.service.ts
@@ -13,13 +13,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Injectable } from '@angular/core';
+import { Injectable, Inject } from '@angular/core';
import { ActivatedRoute, Router} from '@angular/router';
import { LogService } from '../../log.service';
// Angular>=2 workaround for missing definition
declare const InstallTrigger: any;
+const matcher = /<\/?([a-zA-Z0-9]+)*(.*?)\/?>/igm;
+const whitelist: string[] = ['b', 'i', 'p', 'em', 'strong', 'br'];
+const evillist: string[] = ['script', 'style', 'iframe'];
+
+/**
+ * Used with the Window size function;
+ **/
+export interface WindowSize {
+ width: number;
+ height: number;
+}
+
+/**
+ * For the sanitize() and analyze() functions
+ */
+export interface Match {
+ full: string;
+ name: string;
+}
// TODO Move all this trie stuff to its own class
// Angular>=2 Tightened up on types to avoid compiler errors
@@ -133,13 +152,14 @@
@Injectable()
export class FnService {
// internal state
- debugFlags = new Map<string, boolean>([
+ private debugFlags = new Map<string, boolean>([
// [ "LoadingService", true ]
]);
constructor(
private route: ActivatedRoute,
- private log: LogService
+ private log: LogService,
+ @Inject(Window) private w: Window
) {
this.route.queryParams.subscribe(params => {
const debugparam: string = params['debug'];
@@ -149,33 +169,155 @@
log.debug('FnService constructed');
}
- isF(f) {
+ /**
+ * Test if an argument is a function
+ *
+ * Note: the need for this would go away if all functions
+ * were strongly typed
+ */
+ isF(f: any): any {
return typeof f === 'function' ? f : null;
}
- isA(a) {
+ /**
+ * Test if an argument is an array
+ *
+ * Note: the need for this would go away if all arrays
+ * were strongly typed
+ */
+ isA(a: any): any {
// NOTE: Array.isArray() is part of EMCAScript 5.1
return Array.isArray(a) ? a : null;
}
- isS(s) {
+ /**
+ * Test if an argument is a string
+ *
+ * Note: the need for this would go away if all strings
+ * were strongly typed
+ */
+ isS(s: any): string {
return typeof s === 'string' ? s : null;
}
- isO(o) {
+ /**
+ * Test if an argument is an object
+ *
+ * Note: the need for this would go away if all objects
+ * were strongly typed
+ */
+ isO(o: any): Object {
return (o && typeof o === 'object' && o.constructor === Object) ? o : null;
}
-// contains: contains,
-// areFunctions: areFunctions,
-// areFunctionsNonStrict: areFunctionsNonStrict,
-// windowSize: windowSize,
+ /**
+ * Test that an array contains an object
+ */
+ contains(a: any[], x: any): boolean {
+ return this.isA(a) && a.indexOf(x) > -1;
+ }
+
+ /**
+ * Returns width and height of window inner dimensions.
+ * offH, offW : offset width/height are subtracted, if present
+ */
+ windowSize(offH: number = 0, offW: number = 0): WindowSize {
+ return {
+ height: this.w.innerHeight - offH,
+ width: this.w.innerWidth - offW
+ };
+ }
+
+ /**
+ * Returns true if all names in the array are defined as functions
+ * on the given api object; false otherwise.
+ * Also returns false if there are properties on the api that are NOT
+ * listed in the array of names.
+ *
+ * This gets extra complicated when the api Object is an
+ * Angular service - while the functions can be retrieved
+ * by an indexed get, the ownProperties does not show the
+ * functions of the class. We have to dive in to the prototypes
+ * properties to get these - and even then we have to filter
+ * out the constructor and any member variables
+ */
+ areFunctions(api: Object, fnNames: string[]): boolean {
+ const fnLookup: Map<string, boolean> = new Map();
+ let extraFound: boolean = false;
+
+ if (!this.isA(fnNames)) {
+ return false;
+ }
+
+ const n: number = fnNames.length;
+ let i: number;
+ let name: string;
+
+ for (i = 0; i < n; i++) {
+ name = fnNames[i];
+ if (!this.isF(api[name])) {
+ return false;
+ }
+ fnLookup.set(name, true);
+ }
+
+ // check for properties on the API that are not listed in the array,
+ const keys = Object.getOwnPropertyNames(api);
+ if (keys.length === 0) {
+ return true;
+ }
+ // If the api is a class it will have a name,
+ // else it will just be called 'Object'
+ const apiObjectName: string = api.constructor.name;
+ if (apiObjectName === 'Object') {
+ Object.keys(api).forEach((key) => {
+ if (!fnLookup.get(key)) {
+ extraFound = true;
+ }
+ });
+ } else { // It is a class, so its functions will be in the child (prototype)
+ const pObj: Object = Object.getPrototypeOf(api);
+ for ( const key in Object.getOwnPropertyDescriptors(pObj) ) {
+ if (key === 'constructor') { // Filter out constructor
+ continue;
+ }
+ const value = Object.getOwnPropertyDescriptor(pObj, key);
+ // Only compare functions. Look for any not given in the map
+ if (this.isF(value.value) && !fnLookup.get(key)) {
+ extraFound = true;
+ }
+ }
+ }
+ return !extraFound;
+ }
+
+ /**
+ * Returns true if all names in the array are defined as functions
+ * on the given api object; false otherwise. This is a non-strict version
+ * that does not care about other properties on the api.
+ */
+ areFunctionsNonStrict(api, fnNames): boolean {
+ if (!this.isA(fnNames)) {
+ return false;
+ }
+ const n = fnNames.length;
+ let i;
+ let name;
+
+ for (i = 0; i < n; i++) {
+ name = fnNames[i];
+ if (!this.isF(api[name])) {
+ return false;
+ }
+ }
+ return true;
+ }
/**
* Returns true if current browser determined to be a mobile device
*/
isMobile() {
- const ua = window.navigator.userAgent;
+ const ua = this.w.navigator.userAgent;
const patt = /iPhone|iPod|iPad|Silk|Android|BlackBerry|Opera Mini|IEMobile/;
return patt.test(ua);
}
@@ -184,10 +326,10 @@
* Returns true if the current browser determined to be Chrome
*/
isChrome() {
- const isChromium = (window as any).chrome;
- const vendorName = window.navigator.vendor;
+ const isChromium = (this.w as any).chrome;
+ const vendorName = this.w.navigator.vendor;
- const isOpera = window.navigator.userAgent.indexOf('OPR') > -1;
+ const isOpera = this.w.navigator.userAgent.indexOf('OPR') > -1;
return (isChromium !== null &&
isChromium !== undefined &&
vendorName === 'Google Inc.' &&
@@ -195,8 +337,8 @@
}
isChromeHeadless() {
- const vendorName = window.navigator.vendor;
- const headlessChrome = window.navigator.userAgent.indexOf('HeadlessChrome') > -1;
+ const vendorName = this.w.navigator.vendor;
+ const headlessChrome = this.w.navigator.userAgent.indexOf('HeadlessChrome') > -1;
return (vendorName === 'Google Inc.' && headlessChrome === true);
}
@@ -205,8 +347,8 @@
* Returns true if the current browser determined to be Safari
*/
isSafari() {
- return (window.navigator.userAgent.indexOf('Safari') !== -1 &&
- window.navigator.userAgent.indexOf('Chrome') === -1);
+ return (this.w.navigator.userAgent.indexOf('Safari') !== -1 &&
+ this.w.navigator.userAgent.indexOf('Chrome') === -1);
}
/**
@@ -217,13 +359,90 @@
}
/**
+ * search through an array of objects, looking for the one with the
+ * tagged property matching the given key. tag defaults to 'id'.
+ * returns the index of the matching object, or -1 for no match.
+ */
+ find(key: string, array: Object[], tag: string = 'id'): number {
+ let idx: number;
+ const n: number = array.length;
+
+ for (idx = 0 ; idx < n; idx++) {
+ const d: Object = array[idx];
+ if (d[tag] === key) {
+ return idx;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * search through array to find (the first occurrence of) item,
+ * returning its index if found; otherwise returning -1.
+ */
+ inArray(item: any, array: any[]): number {
+ if (this.isA(array)) {
+ for (let i = 0; i < array.length; i++) {
+ if (array[i] === item) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * remove (the first occurrence of) the specified item from the given
+ * array, if any. Return true if the removal was made; false otherwise.
+ */
+ removeFromArray(item: any, array: any[]): boolean {
+ const i: number = this.inArray(item, array);
+ if (i >= 0) {
+ array.splice(i, 1);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * return true if the object is empty, return false otherwise
+ */
+ isEmptyObject(obj: Object): boolean {
+ for (const key in obj) {
+ if (true) { return false; }
+ }
+ return true;
+ }
+
+ /**
* Return the given string with the first character capitalized.
*/
- cap(s) {
+ cap(s: string): string {
return s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s;
}
/**
+ * return the parameter without a px suffix
+ */
+ noPx(num: string): number {
+ return Number(num.replace(/px$/, ''));
+ }
+
+ /**
+ * return an element's given style property without px suffix
+ */
+ noPxStyle(elem: any, prop: string): number {
+ return Number(elem.style(prop).replace(/px$/, ''));
+ }
+
+ /**
+ * Return true if a str ends with suffix
+ */
+ endsWith(str: string, suffix: string) {
+ return str.indexOf(suffix, str.length - suffix.length) !== -1;
+ }
+
+ /**
* output debug message to console, if debug tag set...
* e.g. fs.debug('mytag', arg1, arg2, ...)
*/
@@ -233,7 +452,7 @@
}
}
- parseDebugFlags(dbgstr: string): void {
+ private parseDebugFlags(dbgstr: string): void {
const bits = dbgstr ? dbgstr.split(',') : [];
bits.forEach((key) => {
this.debugFlags.set(key, true);
@@ -248,4 +467,66 @@
return this.debugFlags.get(tag);
}
+
+
+ // -----------------------------------------------------------------
+ // The next section deals with sanitizing external strings destined
+ // to be loaded via a .html() function call.
+ //
+ // See definition of matcher, evillist and whitelist at the top of this file
+
+ /*
+ * Returns true if the tag is in the evil list, (and is not an end-tag)
+ */
+ inEvilList(tag: any): boolean {
+ return (evillist.indexOf(tag.name) !== -1 && tag.full.indexOf('/') === -1);
+ }
+
+ /*
+ * Returns an array of Matches of matcher in html
+ */
+ analyze(html: string): Match[] {
+ const matches: Match[] = [];
+ let 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;
+ }
+
+ /*
+ * Returns a cleaned version of html
+ */
+ sanitize(html: string): string {
+ const matches: Match[] = this.analyze(html);
+
+ // completely obliterate evil tags and their contents...
+ evillist.forEach((tag) => {
+ const re = new RegExp('<' + tag + '(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/' + tag + '>', 'gim');
+ html = html.replace(re, '');
+ });
+
+ // filter out all but white-listed tags and end-tags
+ matches.forEach((tag) => {
+ if (whitelist.indexOf(tag.name) === -1) {
+ html = html.replace(tag.full, '');
+ if (this.inEvilList(tag)) {
+ this.log.warn('Unsanitary HTML input -- ' +
+ tag.full + ' detected!');
+ }
+ }
+ });
+
+ // TODO: consider encoding HTML entities, e.g. '&' -> '&'
+
+ return html;
+ }
+
}