blob: 506ce741ae6715861d7db7e3c97c54b999d291e6 [file] [log] [blame]
Sean Condon83fc39f2018-04-19 18:56:13 +01001/*
Sean Condon5ca00262018-09-06 17:55:25 +01002 * Copyright 2018-present Open Networking Foundation
Sean Condon83fc39f2018-04-19 18:56:13 +01003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
Sean Condonfd6d11b2018-06-02 20:29:49 +010016import { Injectable, Inject } from '@angular/core';
Sean Condon83fc39f2018-04-19 18:56:13 +010017import { ActivatedRoute, Router} from '@angular/router';
Sean Condon5ca00262018-09-06 17:55:25 +010018import { LogService } from '../log.service';
Sean Condon83fc39f2018-04-19 18:56:13 +010019
20// Angular>=2 workaround for missing definition
21declare const InstallTrigger: any;
22
Sean Condonfd6d11b2018-06-02 20:29:49 +010023const matcher = /<\/?([a-zA-Z0-9]+)*(.*?)\/?>/igm;
24const whitelist: string[] = ['b', 'i', 'p', 'em', 'strong', 'br'];
25const evillist: string[] = ['script', 'style', 'iframe'];
26
27/**
28 * Used with the Window size function;
29 **/
30export interface WindowSize {
31 width: number;
32 height: number;
33}
34
35/**
36 * For the sanitize() and analyze() functions
37 */
38export interface Match {
39 full: string;
40 name: string;
41}
Sean Condon83fc39f2018-04-19 18:56:13 +010042
43// TODO Move all this trie stuff to its own class
44// Angular>=2 Tightened up on types to avoid compiler errors
45interface TrieC {
Sean Condon49e15be2018-05-16 16:58:29 +010046 p: any;
47 s: string[];
Sean Condon83fc39f2018-04-19 18:56:13 +010048}
49// trie operation
50function _trieOp(op: string, trie, word: string, data) {
Sean Condon49e15be2018-05-16 16:58:29 +010051 const p = trie;
52 const w: string = word.toUpperCase();
53 const s: Array<string> = w.split('');
54 let c: TrieC = { p: p, s: s };
55 let t = [];
56 let x = 0;
57 const f1 = op === '+' ? add : probe;
58 const f2 = op === '+' ? insert : remove;
Sean Condon83fc39f2018-04-19 18:56:13 +010059
Sean Condon49e15be2018-05-16 16:58:29 +010060 function add(cAdded): TrieC {
61 const q = cAdded.s.shift();
62 let np = cAdded.p[q];
Sean Condon83fc39f2018-04-19 18:56:13 +010063
64 if (!np) {
Sean Condon49e15be2018-05-16 16:58:29 +010065 cAdded.p[q] = {};
66 np = cAdded.p[q];
Sean Condon83fc39f2018-04-19 18:56:13 +010067 x = 1;
68 }
Sean Condon49e15be2018-05-16 16:58:29 +010069 return { p: np, s: cAdded.s };
Sean Condon83fc39f2018-04-19 18:56:13 +010070 }
71
Sean Condon49e15be2018-05-16 16:58:29 +010072 function probe(cProbed): TrieC {
73 const q = cProbed.s.shift();
74 const k: number = Object.keys(cProbed.p).length;
75 const np = cProbed.p[q];
Sean Condon83fc39f2018-04-19 18:56:13 +010076
Sean Condon49e15be2018-05-16 16:58:29 +010077 t.push({ q: q, k: k, p: cProbed.p });
Sean Condon83fc39f2018-04-19 18:56:13 +010078 if (!np) {
79 t = [];
80 return { p: [], s: [] };
81 }
Sean Condon49e15be2018-05-16 16:58:29 +010082 return { p: np, s: cProbed.s };
Sean Condon83fc39f2018-04-19 18:56:13 +010083 }
84
85 function insert() {
86 c.p._data = data;
87 return x ? 'added' : 'updated';
88 }
89
90 function remove() {
91 if (t.length) {
92 t = t.reverse();
93 while (t.length) {
Sean Condon49e15be2018-05-16 16:58:29 +010094 const d = t.shift();
Sean Condon83fc39f2018-04-19 18:56:13 +010095 delete d.p[d.q];
96 if (d.k > 1) {
97 t = [];
98 }
99 }
100 return 'removed';
101 }
102 return 'absent';
103 }
104
105 while (c.s.length) {
106 c = f1(c);
107 }
108 return f2();
109}
110
111// add word to trie (word will be converted to uppercase)
112// data associated with the word
113// returns 'added' or 'updated'
114function addToTrie(trie, word, data) {
115 return _trieOp('+', trie, word, data);
116}
117
118// remove word from trie (word will be converted to uppercase)
119// returns 'removed' or 'absent'
120// Angular>=2 added in quotes for data. error TS2554: Expected 4 arguments, but got 3.
121function removeFromTrie(trie, word) {
122 return _trieOp('-', trie, word, '');
123}
124
125// lookup word (converted to uppercase) in trie
126// returns:
127// undefined if the word is not in the trie
128// -1 for a partial match (word is a prefix to an existing word)
129// data for the word for an exact match
130function trieLookup(trie, word) {
Sean Condon49e15be2018-05-16 16:58:29 +0100131 const s = word.toUpperCase().split('');
132 let p = trie;
133 let n;
Sean Condon83fc39f2018-04-19 18:56:13 +0100134
135 while (s.length) {
136 n = s.shift();
137 p = p[n];
138 if (!p) {
139 return undefined;
140 }
141 }
142 if (p._data) {
143 return p._data;
144 }
145 return -1;
146}
147
148
149/**
150 * ONOS GUI -- Util -- General Purpose Functions
151 */
Sean Condona00bf382018-06-23 07:54:01 +0100152@Injectable({
153 providedIn: 'root',
154})
Sean Condon83fc39f2018-04-19 18:56:13 +0100155export class FnService {
156 // internal state
Sean Condonfd6d11b2018-06-02 20:29:49 +0100157 private debugFlags = new Map<string, boolean>([
Sean Condon83fc39f2018-04-19 18:56:13 +0100158// [ "LoadingService", true ]
159 ]);
160
161 constructor(
162 private route: ActivatedRoute,
Sean Condonfd6d11b2018-06-02 20:29:49 +0100163 private log: LogService,
Sean Condon5ca00262018-09-06 17:55:25 +0100164 // TODO: Change the any type to Window when https://github.com/angular/angular/issues/15640 is fixed.
165 @Inject('Window') private w: any
Sean Condon83fc39f2018-04-19 18:56:13 +0100166 ) {
167 this.route.queryParams.subscribe(params => {
Sean Condon49e15be2018-05-16 16:58:29 +0100168 const debugparam: string = params['debug'];
Sean Condon5ca00262018-09-06 17:55:25 +0100169// log.debug('Param:', debugparam);
Sean Condon83fc39f2018-04-19 18:56:13 +0100170 this.parseDebugFlags(debugparam);
171 });
Sean Condon5ca00262018-09-06 17:55:25 +0100172// this.log.debug('FnService constructed');
Sean Condon83fc39f2018-04-19 18:56:13 +0100173 }
174
Sean Condonfd6d11b2018-06-02 20:29:49 +0100175 /**
176 * Test if an argument is a function
177 *
178 * Note: the need for this would go away if all functions
179 * were strongly typed
180 */
181 isF(f: any): any {
Sean Condon83fc39f2018-04-19 18:56:13 +0100182 return typeof f === 'function' ? f : null;
183 }
184
Sean Condonfd6d11b2018-06-02 20:29:49 +0100185 /**
186 * Test if an argument is an array
187 *
188 * Note: the need for this would go away if all arrays
189 * were strongly typed
190 */
191 isA(a: any): any {
Sean Condon83fc39f2018-04-19 18:56:13 +0100192 // NOTE: Array.isArray() is part of EMCAScript 5.1
193 return Array.isArray(a) ? a : null;
194 }
195
Sean Condonfd6d11b2018-06-02 20:29:49 +0100196 /**
197 * Test if an argument is a string
198 *
199 * Note: the need for this would go away if all strings
200 * were strongly typed
201 */
202 isS(s: any): string {
Sean Condon83fc39f2018-04-19 18:56:13 +0100203 return typeof s === 'string' ? s : null;
204 }
205
Sean Condonfd6d11b2018-06-02 20:29:49 +0100206 /**
207 * Test if an argument is an object
208 *
209 * Note: the need for this would go away if all objects
210 * were strongly typed
211 */
212 isO(o: any): Object {
Sean Condon83fc39f2018-04-19 18:56:13 +0100213 return (o && typeof o === 'object' && o.constructor === Object) ? o : null;
214 }
215
Sean Condonfd6d11b2018-06-02 20:29:49 +0100216 /**
217 * Test that an array contains an object
218 */
219 contains(a: any[], x: any): boolean {
220 return this.isA(a) && a.indexOf(x) > -1;
221 }
222
223 /**
224 * Returns width and height of window inner dimensions.
225 * offH, offW : offset width/height are subtracted, if present
226 */
227 windowSize(offH: number = 0, offW: number = 0): WindowSize {
228 return {
229 height: this.w.innerHeight - offH,
230 width: this.w.innerWidth - offW
231 };
232 }
233
234 /**
235 * Returns true if all names in the array are defined as functions
236 * on the given api object; false otherwise.
237 * Also returns false if there are properties on the api that are NOT
238 * listed in the array of names.
239 *
240 * This gets extra complicated when the api Object is an
241 * Angular service - while the functions can be retrieved
242 * by an indexed get, the ownProperties does not show the
243 * functions of the class. We have to dive in to the prototypes
244 * properties to get these - and even then we have to filter
245 * out the constructor and any member variables
246 */
247 areFunctions(api: Object, fnNames: string[]): boolean {
248 const fnLookup: Map<string, boolean> = new Map();
249 let extraFound: boolean = false;
250
251 if (!this.isA(fnNames)) {
252 return false;
253 }
254
255 const n: number = fnNames.length;
256 let i: number;
257 let name: string;
258
259 for (i = 0; i < n; i++) {
260 name = fnNames[i];
261 if (!this.isF(api[name])) {
262 return false;
263 }
264 fnLookup.set(name, true);
265 }
266
267 // check for properties on the API that are not listed in the array,
268 const keys = Object.getOwnPropertyNames(api);
269 if (keys.length === 0) {
270 return true;
271 }
272 // If the api is a class it will have a name,
273 // else it will just be called 'Object'
274 const apiObjectName: string = api.constructor.name;
275 if (apiObjectName === 'Object') {
276 Object.keys(api).forEach((key) => {
277 if (!fnLookup.get(key)) {
278 extraFound = true;
279 }
280 });
281 } else { // It is a class, so its functions will be in the child (prototype)
282 const pObj: Object = Object.getPrototypeOf(api);
283 for ( const key in Object.getOwnPropertyDescriptors(pObj) ) {
284 if (key === 'constructor') { // Filter out constructor
285 continue;
286 }
287 const value = Object.getOwnPropertyDescriptor(pObj, key);
288 // Only compare functions. Look for any not given in the map
289 if (this.isF(value.value) && !fnLookup.get(key)) {
290 extraFound = true;
291 }
292 }
293 }
294 return !extraFound;
295 }
296
297 /**
298 * Returns true if all names in the array are defined as functions
299 * on the given api object; false otherwise. This is a non-strict version
300 * that does not care about other properties on the api.
301 */
302 areFunctionsNonStrict(api, fnNames): boolean {
303 if (!this.isA(fnNames)) {
304 return false;
305 }
306 const n = fnNames.length;
307 let i;
308 let name;
309
310 for (i = 0; i < n; i++) {
311 name = fnNames[i];
312 if (!this.isF(api[name])) {
313 return false;
314 }
315 }
316 return true;
317 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100318
319 /**
320 * Returns true if current browser determined to be a mobile device
321 */
322 isMobile() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100323 const ua = this.w.navigator.userAgent;
Sean Condon49e15be2018-05-16 16:58:29 +0100324 const patt = /iPhone|iPod|iPad|Silk|Android|BlackBerry|Opera Mini|IEMobile/;
Sean Condon83fc39f2018-04-19 18:56:13 +0100325 return patt.test(ua);
326 }
327
328 /**
329 * Returns true if the current browser determined to be Chrome
330 */
331 isChrome() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100332 const isChromium = (this.w as any).chrome;
333 const vendorName = this.w.navigator.vendor;
Sean Condon83fc39f2018-04-19 18:56:13 +0100334
Sean Condonfd6d11b2018-06-02 20:29:49 +0100335 const isOpera = this.w.navigator.userAgent.indexOf('OPR') > -1;
Sean Condon83fc39f2018-04-19 18:56:13 +0100336 return (isChromium !== null &&
337 isChromium !== undefined &&
338 vendorName === 'Google Inc.' &&
Sean Condon49e15be2018-05-16 16:58:29 +0100339 isOpera === false);
340 }
341
342 isChromeHeadless() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100343 const vendorName = this.w.navigator.vendor;
344 const headlessChrome = this.w.navigator.userAgent.indexOf('HeadlessChrome') > -1;
Sean Condon49e15be2018-05-16 16:58:29 +0100345
346 return (vendorName === 'Google Inc.' && headlessChrome === true);
Sean Condon83fc39f2018-04-19 18:56:13 +0100347 }
348
349 /**
350 * Returns true if the current browser determined to be Safari
351 */
352 isSafari() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100353 return (this.w.navigator.userAgent.indexOf('Safari') !== -1 &&
354 this.w.navigator.userAgent.indexOf('Chrome') === -1);
Sean Condon83fc39f2018-04-19 18:56:13 +0100355 }
356
357 /**
358 * Returns true if the current browser determined to be Firefox
359 */
360 isFirefox() {
361 return typeof InstallTrigger !== 'undefined';
362 }
363
364 /**
Sean Condonfd6d11b2018-06-02 20:29:49 +0100365 * search through an array of objects, looking for the one with the
366 * tagged property matching the given key. tag defaults to 'id'.
367 * returns the index of the matching object, or -1 for no match.
368 */
369 find(key: string, array: Object[], tag: string = 'id'): number {
370 let idx: number;
371 const n: number = array.length;
372
373 for (idx = 0 ; idx < n; idx++) {
374 const d: Object = array[idx];
375 if (d[tag] === key) {
376 return idx;
377 }
378 }
379 return -1;
380 }
381
382 /**
383 * search through array to find (the first occurrence of) item,
384 * returning its index if found; otherwise returning -1.
385 */
386 inArray(item: any, array: any[]): number {
387 if (this.isA(array)) {
388 for (let i = 0; i < array.length; i++) {
389 if (array[i] === item) {
390 return i;
391 }
392 }
393 }
394 return -1;
395 }
396
397 /**
398 * remove (the first occurrence of) the specified item from the given
399 * array, if any. Return true if the removal was made; false otherwise.
400 */
401 removeFromArray(item: any, array: any[]): boolean {
402 const i: number = this.inArray(item, array);
403 if (i >= 0) {
404 array.splice(i, 1);
405 return true;
406 }
407 return false;
408 }
409
410 /**
411 * return true if the object is empty, return false otherwise
412 */
413 isEmptyObject(obj: Object): boolean {
414 for (const key in obj) {
415 if (true) { return false; }
416 }
417 return true;
418 }
419
420 /**
Sean Condon2bd11b72018-06-15 08:00:48 +0100421 * returns true if the two objects have all the same properties
422 */
423 sameObjProps(obj1: Object, obj2: Object): boolean {
424 for (const key in obj1) {
425 if (obj1.hasOwnProperty(key)) {
426 if (!(obj1[key] === obj2[key])) {
427 return false;
428 }
429 }
430 }
431 return true;
432 }
433
434 /**
435 * returns true if the array contains the object
436 * does NOT use strict object reference equality,
437 * instead checks each property individually for equality
438 */
439 containsObj(arr: any[], obj: Object): boolean {
440 const len = arr.length;
441 for (let i = 0; i < len; i++) {
442 if (this.sameObjProps(arr[i], obj)) {
443 return true;
444 }
445 }
446 return false;
447 }
448
449 /**
Sean Condon83fc39f2018-04-19 18:56:13 +0100450 * Return the given string with the first character capitalized.
451 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100452 cap(s: string): string {
Sean Condon83fc39f2018-04-19 18:56:13 +0100453 return s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s;
454 }
455
456 /**
Sean Condonfd6d11b2018-06-02 20:29:49 +0100457 * return the parameter without a px suffix
458 */
459 noPx(num: string): number {
460 return Number(num.replace(/px$/, ''));
461 }
462
463 /**
464 * return an element's given style property without px suffix
465 */
466 noPxStyle(elem: any, prop: string): number {
467 return Number(elem.style(prop).replace(/px$/, ''));
468 }
469
470 /**
471 * Return true if a str ends with suffix
472 */
473 endsWith(str: string, suffix: string) {
474 return str.indexOf(suffix, str.length - suffix.length) !== -1;
475 }
476
477 /**
Sean Condon83fc39f2018-04-19 18:56:13 +0100478 * output debug message to console, if debug tag set...
479 * e.g. fs.debug('mytag', arg1, arg2, ...)
480 */
481 debug(tag, ...args) {
482 if (this.debugFlags.get(tag)) {
Sean Condon5ca00262018-09-06 17:55:25 +0100483// this.log.debug(tag, args.join());
Sean Condon83fc39f2018-04-19 18:56:13 +0100484 }
485 }
486
Sean Condonfd6d11b2018-06-02 20:29:49 +0100487 private parseDebugFlags(dbgstr: string): void {
Sean Condon49e15be2018-05-16 16:58:29 +0100488 const bits = dbgstr ? dbgstr.split(',') : [];
489 bits.forEach((key) => {
Sean Condon83fc39f2018-04-19 18:56:13 +0100490 this.debugFlags.set(key, true);
491 });
Sean Condon5ca00262018-09-06 17:55:25 +0100492// this.log.debug('Debug flags:', dbgstr);
Sean Condon83fc39f2018-04-19 18:56:13 +0100493 }
494
495 /**
496 * Return true if the given debug flag was specified in the query params
497 */
498 debugOn(tag: string): boolean {
499 return this.debugFlags.get(tag);
500 }
501
Sean Condonfd6d11b2018-06-02 20:29:49 +0100502
503
504 // -----------------------------------------------------------------
505 // The next section deals with sanitizing external strings destined
506 // to be loaded via a .html() function call.
507 //
508 // See definition of matcher, evillist and whitelist at the top of this file
509
510 /*
511 * Returns true if the tag is in the evil list, (and is not an end-tag)
512 */
513 inEvilList(tag: any): boolean {
514 return (evillist.indexOf(tag.name) !== -1 && tag.full.indexOf('/') === -1);
515 }
516
517 /*
518 * Returns an array of Matches of matcher in html
519 */
520 analyze(html: string): Match[] {
521 const matches: Match[] = [];
522 let match;
523
524 // extract all tags
525 while ((match = matcher.exec(html)) !== null) {
526 matches.push({
527 full: match[0],
528 name: match[1],
529 // NOTE: ignoring attributes {match[2].split(' ')} for now
530 });
531 }
532
533 return matches;
534 }
535
536 /*
537 * Returns a cleaned version of html
538 */
539 sanitize(html: string): string {
540 const matches: Match[] = this.analyze(html);
541
542 // completely obliterate evil tags and their contents...
543 evillist.forEach((tag) => {
544 const re = new RegExp('<' + tag + '(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/' + tag + '>', 'gim');
545 html = html.replace(re, '');
546 });
547
548 // filter out all but white-listed tags and end-tags
549 matches.forEach((tag) => {
550 if (whitelist.indexOf(tag.name) === -1) {
551 html = html.replace(tag.full, '');
552 if (this.inEvilList(tag)) {
553 this.log.warn('Unsanitary HTML input -- ' +
554 tag.full + ' detected!');
555 }
556 }
557 });
558
559 // TODO: consider encoding HTML entities, e.g. '&' -> '&amp;'
560
561 return html;
562 }
563
Sean Condon83fc39f2018-04-19 18:56:13 +0100564}