blob: d0307e32276b1eeb35dae995512aed9b563470ad [file] [log] [blame]
Sean Condon83fc39f2018-04-19 18:56:13 +01001/*
2 * Copyright 2014-present Open Networking Foundation
3 *
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';
18import { LogService } from '../../log.service';
19
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 Condona00bf382018-06-23 07:54:01 +0100164 @Inject('Window') private w: Window
Sean Condon83fc39f2018-04-19 18:56:13 +0100165 ) {
166 this.route.queryParams.subscribe(params => {
Sean Condon49e15be2018-05-16 16:58:29 +0100167 const debugparam: string = params['debug'];
168 log.debug('Param:', debugparam);
Sean Condon83fc39f2018-04-19 18:56:13 +0100169 this.parseDebugFlags(debugparam);
170 });
Sean Condona00bf382018-06-23 07:54:01 +0100171 this.log.debug('FnService constructed');
Sean Condon83fc39f2018-04-19 18:56:13 +0100172 }
173
Sean Condonfd6d11b2018-06-02 20:29:49 +0100174 /**
175 * Test if an argument is a function
176 *
177 * Note: the need for this would go away if all functions
178 * were strongly typed
179 */
180 isF(f: any): any {
Sean Condon83fc39f2018-04-19 18:56:13 +0100181 return typeof f === 'function' ? f : null;
182 }
183
Sean Condonfd6d11b2018-06-02 20:29:49 +0100184 /**
185 * Test if an argument is an array
186 *
187 * Note: the need for this would go away if all arrays
188 * were strongly typed
189 */
190 isA(a: any): any {
Sean Condon83fc39f2018-04-19 18:56:13 +0100191 // NOTE: Array.isArray() is part of EMCAScript 5.1
192 return Array.isArray(a) ? a : null;
193 }
194
Sean Condonfd6d11b2018-06-02 20:29:49 +0100195 /**
196 * Test if an argument is a string
197 *
198 * Note: the need for this would go away if all strings
199 * were strongly typed
200 */
201 isS(s: any): string {
Sean Condon83fc39f2018-04-19 18:56:13 +0100202 return typeof s === 'string' ? s : null;
203 }
204
Sean Condonfd6d11b2018-06-02 20:29:49 +0100205 /**
206 * Test if an argument is an object
207 *
208 * Note: the need for this would go away if all objects
209 * were strongly typed
210 */
211 isO(o: any): Object {
Sean Condon83fc39f2018-04-19 18:56:13 +0100212 return (o && typeof o === 'object' && o.constructor === Object) ? o : null;
213 }
214
Sean Condonfd6d11b2018-06-02 20:29:49 +0100215 /**
216 * Test that an array contains an object
217 */
218 contains(a: any[], x: any): boolean {
219 return this.isA(a) && a.indexOf(x) > -1;
220 }
221
222 /**
223 * Returns width and height of window inner dimensions.
224 * offH, offW : offset width/height are subtracted, if present
225 */
226 windowSize(offH: number = 0, offW: number = 0): WindowSize {
227 return {
228 height: this.w.innerHeight - offH,
229 width: this.w.innerWidth - offW
230 };
231 }
232
233 /**
234 * Returns true if all names in the array are defined as functions
235 * on the given api object; false otherwise.
236 * Also returns false if there are properties on the api that are NOT
237 * listed in the array of names.
238 *
239 * This gets extra complicated when the api Object is an
240 * Angular service - while the functions can be retrieved
241 * by an indexed get, the ownProperties does not show the
242 * functions of the class. We have to dive in to the prototypes
243 * properties to get these - and even then we have to filter
244 * out the constructor and any member variables
245 */
246 areFunctions(api: Object, fnNames: string[]): boolean {
247 const fnLookup: Map<string, boolean> = new Map();
248 let extraFound: boolean = false;
249
250 if (!this.isA(fnNames)) {
251 return false;
252 }
253
254 const n: number = fnNames.length;
255 let i: number;
256 let name: string;
257
258 for (i = 0; i < n; i++) {
259 name = fnNames[i];
260 if (!this.isF(api[name])) {
261 return false;
262 }
263 fnLookup.set(name, true);
264 }
265
266 // check for properties on the API that are not listed in the array,
267 const keys = Object.getOwnPropertyNames(api);
268 if (keys.length === 0) {
269 return true;
270 }
271 // If the api is a class it will have a name,
272 // else it will just be called 'Object'
273 const apiObjectName: string = api.constructor.name;
274 if (apiObjectName === 'Object') {
275 Object.keys(api).forEach((key) => {
276 if (!fnLookup.get(key)) {
277 extraFound = true;
278 }
279 });
280 } else { // It is a class, so its functions will be in the child (prototype)
281 const pObj: Object = Object.getPrototypeOf(api);
282 for ( const key in Object.getOwnPropertyDescriptors(pObj) ) {
283 if (key === 'constructor') { // Filter out constructor
284 continue;
285 }
286 const value = Object.getOwnPropertyDescriptor(pObj, key);
287 // Only compare functions. Look for any not given in the map
288 if (this.isF(value.value) && !fnLookup.get(key)) {
289 extraFound = true;
290 }
291 }
292 }
293 return !extraFound;
294 }
295
296 /**
297 * Returns true if all names in the array are defined as functions
298 * on the given api object; false otherwise. This is a non-strict version
299 * that does not care about other properties on the api.
300 */
301 areFunctionsNonStrict(api, fnNames): boolean {
302 if (!this.isA(fnNames)) {
303 return false;
304 }
305 const n = fnNames.length;
306 let i;
307 let name;
308
309 for (i = 0; i < n; i++) {
310 name = fnNames[i];
311 if (!this.isF(api[name])) {
312 return false;
313 }
314 }
315 return true;
316 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100317
318 /**
319 * Returns true if current browser determined to be a mobile device
320 */
321 isMobile() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100322 const ua = this.w.navigator.userAgent;
Sean Condon49e15be2018-05-16 16:58:29 +0100323 const patt = /iPhone|iPod|iPad|Silk|Android|BlackBerry|Opera Mini|IEMobile/;
Sean Condon83fc39f2018-04-19 18:56:13 +0100324 return patt.test(ua);
325 }
326
327 /**
328 * Returns true if the current browser determined to be Chrome
329 */
330 isChrome() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100331 const isChromium = (this.w as any).chrome;
332 const vendorName = this.w.navigator.vendor;
Sean Condon83fc39f2018-04-19 18:56:13 +0100333
Sean Condonfd6d11b2018-06-02 20:29:49 +0100334 const isOpera = this.w.navigator.userAgent.indexOf('OPR') > -1;
Sean Condon83fc39f2018-04-19 18:56:13 +0100335 return (isChromium !== null &&
336 isChromium !== undefined &&
337 vendorName === 'Google Inc.' &&
Sean Condon49e15be2018-05-16 16:58:29 +0100338 isOpera === false);
339 }
340
341 isChromeHeadless() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100342 const vendorName = this.w.navigator.vendor;
343 const headlessChrome = this.w.navigator.userAgent.indexOf('HeadlessChrome') > -1;
Sean Condon49e15be2018-05-16 16:58:29 +0100344
345 return (vendorName === 'Google Inc.' && headlessChrome === true);
Sean Condon83fc39f2018-04-19 18:56:13 +0100346 }
347
348 /**
349 * Returns true if the current browser determined to be Safari
350 */
351 isSafari() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100352 return (this.w.navigator.userAgent.indexOf('Safari') !== -1 &&
353 this.w.navigator.userAgent.indexOf('Chrome') === -1);
Sean Condon83fc39f2018-04-19 18:56:13 +0100354 }
355
356 /**
357 * Returns true if the current browser determined to be Firefox
358 */
359 isFirefox() {
360 return typeof InstallTrigger !== 'undefined';
361 }
362
363 /**
Sean Condonfd6d11b2018-06-02 20:29:49 +0100364 * search through an array of objects, looking for the one with the
365 * tagged property matching the given key. tag defaults to 'id'.
366 * returns the index of the matching object, or -1 for no match.
367 */
368 find(key: string, array: Object[], tag: string = 'id'): number {
369 let idx: number;
370 const n: number = array.length;
371
372 for (idx = 0 ; idx < n; idx++) {
373 const d: Object = array[idx];
374 if (d[tag] === key) {
375 return idx;
376 }
377 }
378 return -1;
379 }
380
381 /**
382 * search through array to find (the first occurrence of) item,
383 * returning its index if found; otherwise returning -1.
384 */
385 inArray(item: any, array: any[]): number {
386 if (this.isA(array)) {
387 for (let i = 0; i < array.length; i++) {
388 if (array[i] === item) {
389 return i;
390 }
391 }
392 }
393 return -1;
394 }
395
396 /**
397 * remove (the first occurrence of) the specified item from the given
398 * array, if any. Return true if the removal was made; false otherwise.
399 */
400 removeFromArray(item: any, array: any[]): boolean {
401 const i: number = this.inArray(item, array);
402 if (i >= 0) {
403 array.splice(i, 1);
404 return true;
405 }
406 return false;
407 }
408
409 /**
410 * return true if the object is empty, return false otherwise
411 */
412 isEmptyObject(obj: Object): boolean {
413 for (const key in obj) {
414 if (true) { return false; }
415 }
416 return true;
417 }
418
419 /**
Sean Condon2bd11b72018-06-15 08:00:48 +0100420 * returns true if the two objects have all the same properties
421 */
422 sameObjProps(obj1: Object, obj2: Object): boolean {
423 for (const key in obj1) {
424 if (obj1.hasOwnProperty(key)) {
425 if (!(obj1[key] === obj2[key])) {
426 return false;
427 }
428 }
429 }
430 return true;
431 }
432
433 /**
434 * returns true if the array contains the object
435 * does NOT use strict object reference equality,
436 * instead checks each property individually for equality
437 */
438 containsObj(arr: any[], obj: Object): boolean {
439 const len = arr.length;
440 for (let i = 0; i < len; i++) {
441 if (this.sameObjProps(arr[i], obj)) {
442 return true;
443 }
444 }
445 return false;
446 }
447
448 /**
Sean Condon83fc39f2018-04-19 18:56:13 +0100449 * Return the given string with the first character capitalized.
450 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100451 cap(s: string): string {
Sean Condon83fc39f2018-04-19 18:56:13 +0100452 return s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s;
453 }
454
455 /**
Sean Condonfd6d11b2018-06-02 20:29:49 +0100456 * return the parameter without a px suffix
457 */
458 noPx(num: string): number {
459 return Number(num.replace(/px$/, ''));
460 }
461
462 /**
463 * return an element's given style property without px suffix
464 */
465 noPxStyle(elem: any, prop: string): number {
466 return Number(elem.style(prop).replace(/px$/, ''));
467 }
468
469 /**
470 * Return true if a str ends with suffix
471 */
472 endsWith(str: string, suffix: string) {
473 return str.indexOf(suffix, str.length - suffix.length) !== -1;
474 }
475
476 /**
Sean Condon83fc39f2018-04-19 18:56:13 +0100477 * output debug message to console, if debug tag set...
478 * e.g. fs.debug('mytag', arg1, arg2, ...)
479 */
480 debug(tag, ...args) {
481 if (this.debugFlags.get(tag)) {
482 this.log.debug(tag, args.join());
483 }
484 }
485
Sean Condonfd6d11b2018-06-02 20:29:49 +0100486 private parseDebugFlags(dbgstr: string): void {
Sean Condon49e15be2018-05-16 16:58:29 +0100487 const bits = dbgstr ? dbgstr.split(',') : [];
488 bits.forEach((key) => {
Sean Condon83fc39f2018-04-19 18:56:13 +0100489 this.debugFlags.set(key, true);
490 });
491 this.log.debug('Debug flags:', dbgstr);
492 }
493
494 /**
495 * Return true if the given debug flag was specified in the query params
496 */
497 debugOn(tag: string): boolean {
498 return this.debugFlags.get(tag);
499 }
500
Sean Condonfd6d11b2018-06-02 20:29:49 +0100501
502
503 // -----------------------------------------------------------------
504 // The next section deals with sanitizing external strings destined
505 // to be loaded via a .html() function call.
506 //
507 // See definition of matcher, evillist and whitelist at the top of this file
508
509 /*
510 * Returns true if the tag is in the evil list, (and is not an end-tag)
511 */
512 inEvilList(tag: any): boolean {
513 return (evillist.indexOf(tag.name) !== -1 && tag.full.indexOf('/') === -1);
514 }
515
516 /*
517 * Returns an array of Matches of matcher in html
518 */
519 analyze(html: string): Match[] {
520 const matches: Match[] = [];
521 let match;
522
523 // extract all tags
524 while ((match = matcher.exec(html)) !== null) {
525 matches.push({
526 full: match[0],
527 name: match[1],
528 // NOTE: ignoring attributes {match[2].split(' ')} for now
529 });
530 }
531
532 return matches;
533 }
534
535 /*
536 * Returns a cleaned version of html
537 */
538 sanitize(html: string): string {
539 const matches: Match[] = this.analyze(html);
540
541 // completely obliterate evil tags and their contents...
542 evillist.forEach((tag) => {
543 const re = new RegExp('<' + tag + '(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/' + tag + '>', 'gim');
544 html = html.replace(re, '');
545 });
546
547 // filter out all but white-listed tags and end-tags
548 matches.forEach((tag) => {
549 if (whitelist.indexOf(tag.name) === -1) {
550 html = html.replace(tag.full, '');
551 if (this.inEvilList(tag)) {
552 this.log.warn('Unsanitary HTML input -- ' +
553 tag.full + ' detected!');
554 }
555 }
556 });
557
558 // TODO: consider encoding HTML entities, e.g. '&' -> '&amp;'
559
560 return html;
561 }
562
Sean Condon83fc39f2018-04-19 18:56:13 +0100563}