blob: 60d49506298bc1cc7e712b157ba0b3e618885d1a [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 Condonf4f54a12018-10-10 23:25:46 +010016import {Inject, Injectable} from '@angular/core';
17import {ActivatedRoute} from '@angular/router';
18import {LogService} from '../log.service';
19import {Trie, TrieOp} from './trie';
Sean Condon83fc39f2018-04-19 18:56:13 +010020
21// Angular>=2 workaround for missing definition
22declare const InstallTrigger: any;
23
Sean Condonfd6d11b2018-06-02 20:29:49 +010024const matcher = /<\/?([a-zA-Z0-9]+)*(.*?)\/?>/igm;
25const whitelist: string[] = ['b', 'i', 'p', 'em', 'strong', 'br'];
26const evillist: string[] = ['script', 'style', 'iframe'];
27
28/**
29 * Used with the Window size function;
30 **/
31export interface WindowSize {
32 width: number;
33 height: number;
34}
35
36/**
37 * For the sanitize() and analyze() functions
38 */
39export interface Match {
40 full: string;
41 name: string;
42}
Sean Condon83fc39f2018-04-19 18:56:13 +010043
Sean Condon83fc39f2018-04-19 18:56:13 +010044/**
45 * ONOS GUI -- Util -- General Purpose Functions
46 */
Sean Condona00bf382018-06-23 07:54:01 +010047@Injectable({
48 providedIn: 'root',
49})
Sean Condon83fc39f2018-04-19 18:56:13 +010050export class FnService {
51 // internal state
Sean Condonfd6d11b2018-06-02 20:29:49 +010052 private debugFlags = new Map<string, boolean>([
Sean Condon83fc39f2018-04-19 18:56:13 +010053// [ "LoadingService", true ]
54 ]);
55
56 constructor(
57 private route: ActivatedRoute,
Sean Condonfd6d11b2018-06-02 20:29:49 +010058 private log: LogService,
Sean Condon5ca00262018-09-06 17:55:25 +010059 // TODO: Change the any type to Window when https://github.com/angular/angular/issues/15640 is fixed.
60 @Inject('Window') private w: any
Sean Condon83fc39f2018-04-19 18:56:13 +010061 ) {
62 this.route.queryParams.subscribe(params => {
Sean Condon49e15be2018-05-16 16:58:29 +010063 const debugparam: string = params['debug'];
Sean Condon5ca00262018-09-06 17:55:25 +010064// log.debug('Param:', debugparam);
Sean Condon83fc39f2018-04-19 18:56:13 +010065 this.parseDebugFlags(debugparam);
66 });
Sean Condon5ca00262018-09-06 17:55:25 +010067// this.log.debug('FnService constructed');
Sean Condon83fc39f2018-04-19 18:56:13 +010068 }
69
Sean Condonfd6d11b2018-06-02 20:29:49 +010070 /**
71 * Test if an argument is a function
72 *
73 * Note: the need for this would go away if all functions
74 * were strongly typed
75 */
76 isF(f: any): any {
Sean Condon83fc39f2018-04-19 18:56:13 +010077 return typeof f === 'function' ? f : null;
78 }
79
Sean Condonfd6d11b2018-06-02 20:29:49 +010080 /**
81 * Test if an argument is an array
82 *
83 * Note: the need for this would go away if all arrays
84 * were strongly typed
85 */
86 isA(a: any): any {
Sean Condon83fc39f2018-04-19 18:56:13 +010087 // NOTE: Array.isArray() is part of EMCAScript 5.1
88 return Array.isArray(a) ? a : null;
89 }
90
Sean Condonfd6d11b2018-06-02 20:29:49 +010091 /**
92 * Test if an argument is a string
93 *
94 * Note: the need for this would go away if all strings
95 * were strongly typed
96 */
97 isS(s: any): string {
Sean Condon83fc39f2018-04-19 18:56:13 +010098 return typeof s === 'string' ? s : null;
99 }
100
Sean Condonfd6d11b2018-06-02 20:29:49 +0100101 /**
102 * Test if an argument is an object
103 *
104 * Note: the need for this would go away if all objects
105 * were strongly typed
106 */
107 isO(o: any): Object {
Sean Condon83fc39f2018-04-19 18:56:13 +0100108 return (o && typeof o === 'object' && o.constructor === Object) ? o : null;
109 }
110
Sean Condonfd6d11b2018-06-02 20:29:49 +0100111 /**
112 * Test that an array contains an object
113 */
114 contains(a: any[], x: any): boolean {
115 return this.isA(a) && a.indexOf(x) > -1;
116 }
117
118 /**
119 * Returns width and height of window inner dimensions.
120 * offH, offW : offset width/height are subtracted, if present
121 */
122 windowSize(offH: number = 0, offW: number = 0): WindowSize {
123 return {
124 height: this.w.innerHeight - offH,
125 width: this.w.innerWidth - offW
126 };
127 }
128
129 /**
130 * Returns true if all names in the array are defined as functions
131 * on the given api object; false otherwise.
132 * Also returns false if there are properties on the api that are NOT
133 * listed in the array of names.
134 *
135 * This gets extra complicated when the api Object is an
136 * Angular service - while the functions can be retrieved
137 * by an indexed get, the ownProperties does not show the
138 * functions of the class. We have to dive in to the prototypes
139 * properties to get these - and even then we have to filter
140 * out the constructor and any member variables
141 */
Sean Condona3ad7792020-01-04 19:26:34 +0000142 areFunctions(api: Object, fnNames: string[]): boolean {
143 const fnLookup: Map<string, boolean> = new Map();
144 let extraFound: boolean = false;
145
146 if (!this.isA(fnNames)) {
147 return false;
148 }
149
150 const n: number = fnNames.length;
151 let i: number;
152 let name: string;
153
154 for (i = 0; i < n; i++) {
155 name = fnNames[i];
156 if (!this.isF(api[name])) {
157 return false;
158 }
159 fnLookup.set(name, true);
160 }
161
162 // check for properties on the API that are not listed in the array,
163 const keys = Object.getOwnPropertyNames(api);
164 if (keys.length === 0) {
165 return true;
166 }
167 // If the api is a class it will have a name,
168 // else it will just be called 'Object'
169 const apiObjectName: string = api.constructor.name;
170 if (apiObjectName === 'Object') {
171 Object.keys(api).forEach((key) => {
172 if (!fnLookup.get(key)) {
173 extraFound = true;
174 }
175 });
176 } else { // It is a class, so its functions will be in the child (prototype)
177 const pObj: Object = Object.getPrototypeOf(api);
178 for ( const key in Object.getOwnPropertyDescriptors(pObj) ) {
179 if (key === 'constructor') { // Filter out constructor
180 continue;
181 }
182 const value = Object.getOwnPropertyDescriptor(pObj, key);
183 // Only compare functions. Look for any not given in the map
184 if (this.isF(value.value) && !fnLookup.get(key)) {
185 extraFound = true;
186 }
187 }
188 }
189 return !extraFound;
190 }
Sean Condonfd6d11b2018-06-02 20:29:49 +0100191
192 /**
193 * Returns true if all names in the array are defined as functions
194 * on the given api object; false otherwise. This is a non-strict version
195 * that does not care about other properties on the api.
196 */
197 areFunctionsNonStrict(api, fnNames): boolean {
198 if (!this.isA(fnNames)) {
199 return false;
200 }
201 const n = fnNames.length;
202 let i;
203 let name;
204
205 for (i = 0; i < n; i++) {
206 name = fnNames[i];
207 if (!this.isF(api[name])) {
208 return false;
209 }
210 }
211 return true;
212 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100213
214 /**
215 * Returns true if current browser determined to be a mobile device
216 */
217 isMobile() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100218 const ua = this.w.navigator.userAgent;
Sean Condon49e15be2018-05-16 16:58:29 +0100219 const patt = /iPhone|iPod|iPad|Silk|Android|BlackBerry|Opera Mini|IEMobile/;
Sean Condon83fc39f2018-04-19 18:56:13 +0100220 return patt.test(ua);
221 }
222
223 /**
224 * Returns true if the current browser determined to be Chrome
225 */
226 isChrome() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100227 const isChromium = (this.w as any).chrome;
228 const vendorName = this.w.navigator.vendor;
Sean Condon83fc39f2018-04-19 18:56:13 +0100229
Sean Condonfd6d11b2018-06-02 20:29:49 +0100230 const isOpera = this.w.navigator.userAgent.indexOf('OPR') > -1;
Sean Condon83fc39f2018-04-19 18:56:13 +0100231 return (isChromium !== null &&
232 isChromium !== undefined &&
233 vendorName === 'Google Inc.' &&
Sean Condon49e15be2018-05-16 16:58:29 +0100234 isOpera === false);
235 }
236
237 isChromeHeadless() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100238 const vendorName = this.w.navigator.vendor;
239 const headlessChrome = this.w.navigator.userAgent.indexOf('HeadlessChrome') > -1;
Sean Condon49e15be2018-05-16 16:58:29 +0100240
241 return (vendorName === 'Google Inc.' && headlessChrome === true);
Sean Condon83fc39f2018-04-19 18:56:13 +0100242 }
243
244 /**
245 * Returns true if the current browser determined to be Safari
246 */
247 isSafari() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100248 return (this.w.navigator.userAgent.indexOf('Safari') !== -1 &&
249 this.w.navigator.userAgent.indexOf('Chrome') === -1);
Sean Condon83fc39f2018-04-19 18:56:13 +0100250 }
251
252 /**
253 * Returns true if the current browser determined to be Firefox
254 */
255 isFirefox() {
256 return typeof InstallTrigger !== 'undefined';
257 }
258
259 /**
Sean Condonfd6d11b2018-06-02 20:29:49 +0100260 * search through an array of objects, looking for the one with the
261 * tagged property matching the given key. tag defaults to 'id'.
262 * returns the index of the matching object, or -1 for no match.
263 */
264 find(key: string, array: Object[], tag: string = 'id'): number {
265 let idx: number;
266 const n: number = array.length;
267
268 for (idx = 0 ; idx < n; idx++) {
269 const d: Object = array[idx];
270 if (d[tag] === key) {
271 return idx;
272 }
273 }
274 return -1;
275 }
276
277 /**
278 * search through array to find (the first occurrence of) item,
279 * returning its index if found; otherwise returning -1.
280 */
281 inArray(item: any, array: any[]): number {
282 if (this.isA(array)) {
283 for (let i = 0; i < array.length; i++) {
284 if (array[i] === item) {
285 return i;
286 }
287 }
288 }
289 return -1;
290 }
291
292 /**
293 * remove (the first occurrence of) the specified item from the given
294 * array, if any. Return true if the removal was made; false otherwise.
295 */
296 removeFromArray(item: any, array: any[]): boolean {
297 const i: number = this.inArray(item, array);
298 if (i >= 0) {
299 array.splice(i, 1);
300 return true;
301 }
302 return false;
303 }
304
305 /**
306 * return true if the object is empty, return false otherwise
307 */
308 isEmptyObject(obj: Object): boolean {
309 for (const key in obj) {
310 if (true) { return false; }
311 }
312 return true;
313 }
314
315 /**
Sean Condon2bd11b72018-06-15 08:00:48 +0100316 * returns true if the two objects have all the same properties
317 */
318 sameObjProps(obj1: Object, obj2: Object): boolean {
319 for (const key in obj1) {
320 if (obj1.hasOwnProperty(key)) {
321 if (!(obj1[key] === obj2[key])) {
322 return false;
323 }
324 }
325 }
326 return true;
327 }
328
329 /**
330 * returns true if the array contains the object
331 * does NOT use strict object reference equality,
332 * instead checks each property individually for equality
333 */
334 containsObj(arr: any[], obj: Object): boolean {
335 const len = arr.length;
336 for (let i = 0; i < len; i++) {
337 if (this.sameObjProps(arr[i], obj)) {
338 return true;
339 }
340 }
341 return false;
342 }
343
344 /**
Sean Condon83fc39f2018-04-19 18:56:13 +0100345 * Return the given string with the first character capitalized.
346 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100347 cap(s: string): string {
Sean Condon83fc39f2018-04-19 18:56:13 +0100348 return s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s;
349 }
350
351 /**
Sean Condonfd6d11b2018-06-02 20:29:49 +0100352 * return the parameter without a px suffix
353 */
354 noPx(num: string): number {
355 return Number(num.replace(/px$/, ''));
356 }
357
358 /**
359 * return an element's given style property without px suffix
360 */
361 noPxStyle(elem: any, prop: string): number {
362 return Number(elem.style(prop).replace(/px$/, ''));
363 }
364
365 /**
366 * Return true if a str ends with suffix
367 */
368 endsWith(str: string, suffix: string) {
369 return str.indexOf(suffix, str.length - suffix.length) !== -1;
370 }
371
372 /**
Sean Condon83fc39f2018-04-19 18:56:13 +0100373 * output debug message to console, if debug tag set...
374 * e.g. fs.debug('mytag', arg1, arg2, ...)
375 */
376 debug(tag, ...args) {
377 if (this.debugFlags.get(tag)) {
Sean Condon5ca00262018-09-06 17:55:25 +0100378// this.log.debug(tag, args.join());
Sean Condon83fc39f2018-04-19 18:56:13 +0100379 }
380 }
381
Sean Condonfd6d11b2018-06-02 20:29:49 +0100382 private parseDebugFlags(dbgstr: string): void {
Sean Condon49e15be2018-05-16 16:58:29 +0100383 const bits = dbgstr ? dbgstr.split(',') : [];
384 bits.forEach((key) => {
Sean Condon83fc39f2018-04-19 18:56:13 +0100385 this.debugFlags.set(key, true);
386 });
Sean Condon5ca00262018-09-06 17:55:25 +0100387// this.log.debug('Debug flags:', dbgstr);
Sean Condon83fc39f2018-04-19 18:56:13 +0100388 }
389
390 /**
391 * Return true if the given debug flag was specified in the query params
392 */
393 debugOn(tag: string): boolean {
394 return this.debugFlags.get(tag);
395 }
396
Sean Condonfd6d11b2018-06-02 20:29:49 +0100397
398
399 // -----------------------------------------------------------------
400 // The next section deals with sanitizing external strings destined
401 // to be loaded via a .html() function call.
402 //
403 // See definition of matcher, evillist and whitelist at the top of this file
404
405 /*
406 * Returns true if the tag is in the evil list, (and is not an end-tag)
407 */
408 inEvilList(tag: any): boolean {
409 return (evillist.indexOf(tag.name) !== -1 && tag.full.indexOf('/') === -1);
410 }
411
412 /*
413 * Returns an array of Matches of matcher in html
414 */
415 analyze(html: string): Match[] {
416 const matches: Match[] = [];
417 let match;
418
419 // extract all tags
420 while ((match = matcher.exec(html)) !== null) {
421 matches.push({
422 full: match[0],
423 name: match[1],
424 // NOTE: ignoring attributes {match[2].split(' ')} for now
425 });
426 }
427
428 return matches;
429 }
430
431 /*
432 * Returns a cleaned version of html
433 */
434 sanitize(html: string): string {
435 const matches: Match[] = this.analyze(html);
436
437 // completely obliterate evil tags and their contents...
438 evillist.forEach((tag) => {
439 const re = new RegExp('<' + tag + '(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/' + tag + '>', 'gim');
440 html = html.replace(re, '');
441 });
442
443 // filter out all but white-listed tags and end-tags
444 matches.forEach((tag) => {
445 if (whitelist.indexOf(tag.name) === -1) {
446 html = html.replace(tag.full, '');
447 if (this.inEvilList(tag)) {
448 this.log.warn('Unsanitary HTML input -- ' +
449 tag.full + ' detected!');
450 }
451 }
452 });
453
454 // TODO: consider encoding HTML entities, e.g. '&' -> '&amp;'
455
456 return html;
457 }
458
Sean Condonf4f54a12018-10-10 23:25:46 +0100459 /**
460 * add word to trie (word will be converted to uppercase)
461 * data associated with the word
462 * returns 'added' or 'updated'
463 */
464 addToTrie(trie, word, data) {
465 return new Trie(TrieOp.PLUS, trie, word, data);
466 }
467
468 /**
469 * remove word from trie (word will be converted to uppercase)
470 * returns 'removed' or 'absent'
471 */
472 removeFromTrie(trie, word) {
473 return new Trie(TrieOp.MINUS, trie, word);
474 }
475
476 /**
477 * lookup word (converted to uppercase) in trie
478 * returns:
479 * undefined if the word is not in the trie
480 * -1 for a partial match (word is a prefix to an existing word)
481 * data for the word for an exact match
482 */
483 trieLookup(trie, word) {
484 const s = word.toUpperCase().split('');
485 let p = trie;
486 let n;
487
488 while (s.length) {
489 n = s.shift();
490 p = p[n];
491 if (!p) {
492 return undefined;
493 }
494 }
495 if (p._data) {
496 return p._data;
497 }
498 return -1;
499 }
500
Sean Condon83fc39f2018-04-19 18:56:13 +0100501}