blob: d355ce9bee559f885bff99fd7c3c04dabcd5bbe6 [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 */
152@Injectable()
153export class FnService {
154 // internal state
Sean Condonfd6d11b2018-06-02 20:29:49 +0100155 private debugFlags = new Map<string, boolean>([
Sean Condon83fc39f2018-04-19 18:56:13 +0100156// [ "LoadingService", true ]
157 ]);
158
159 constructor(
160 private route: ActivatedRoute,
Sean Condonfd6d11b2018-06-02 20:29:49 +0100161 private log: LogService,
162 @Inject(Window) private w: Window
Sean Condon83fc39f2018-04-19 18:56:13 +0100163 ) {
164 this.route.queryParams.subscribe(params => {
Sean Condon49e15be2018-05-16 16:58:29 +0100165 const debugparam: string = params['debug'];
166 log.debug('Param:', debugparam);
Sean Condon83fc39f2018-04-19 18:56:13 +0100167 this.parseDebugFlags(debugparam);
168 });
Sean Condon49e15be2018-05-16 16:58:29 +0100169 log.debug('FnService constructed');
Sean Condon83fc39f2018-04-19 18:56:13 +0100170 }
171
Sean Condonfd6d11b2018-06-02 20:29:49 +0100172 /**
173 * Test if an argument is a function
174 *
175 * Note: the need for this would go away if all functions
176 * were strongly typed
177 */
178 isF(f: any): any {
Sean Condon83fc39f2018-04-19 18:56:13 +0100179 return typeof f === 'function' ? f : null;
180 }
181
Sean Condonfd6d11b2018-06-02 20:29:49 +0100182 /**
183 * Test if an argument is an array
184 *
185 * Note: the need for this would go away if all arrays
186 * were strongly typed
187 */
188 isA(a: any): any {
Sean Condon83fc39f2018-04-19 18:56:13 +0100189 // NOTE: Array.isArray() is part of EMCAScript 5.1
190 return Array.isArray(a) ? a : null;
191 }
192
Sean Condonfd6d11b2018-06-02 20:29:49 +0100193 /**
194 * Test if an argument is a string
195 *
196 * Note: the need for this would go away if all strings
197 * were strongly typed
198 */
199 isS(s: any): string {
Sean Condon83fc39f2018-04-19 18:56:13 +0100200 return typeof s === 'string' ? s : null;
201 }
202
Sean Condonfd6d11b2018-06-02 20:29:49 +0100203 /**
204 * Test if an argument is an object
205 *
206 * Note: the need for this would go away if all objects
207 * were strongly typed
208 */
209 isO(o: any): Object {
Sean Condon83fc39f2018-04-19 18:56:13 +0100210 return (o && typeof o === 'object' && o.constructor === Object) ? o : null;
211 }
212
Sean Condonfd6d11b2018-06-02 20:29:49 +0100213 /**
214 * Test that an array contains an object
215 */
216 contains(a: any[], x: any): boolean {
217 return this.isA(a) && a.indexOf(x) > -1;
218 }
219
220 /**
221 * Returns width and height of window inner dimensions.
222 * offH, offW : offset width/height are subtracted, if present
223 */
224 windowSize(offH: number = 0, offW: number = 0): WindowSize {
225 return {
226 height: this.w.innerHeight - offH,
227 width: this.w.innerWidth - offW
228 };
229 }
230
231 /**
232 * Returns true if all names in the array are defined as functions
233 * on the given api object; false otherwise.
234 * Also returns false if there are properties on the api that are NOT
235 * listed in the array of names.
236 *
237 * This gets extra complicated when the api Object is an
238 * Angular service - while the functions can be retrieved
239 * by an indexed get, the ownProperties does not show the
240 * functions of the class. We have to dive in to the prototypes
241 * properties to get these - and even then we have to filter
242 * out the constructor and any member variables
243 */
244 areFunctions(api: Object, fnNames: string[]): boolean {
245 const fnLookup: Map<string, boolean> = new Map();
246 let extraFound: boolean = false;
247
248 if (!this.isA(fnNames)) {
249 return false;
250 }
251
252 const n: number = fnNames.length;
253 let i: number;
254 let name: string;
255
256 for (i = 0; i < n; i++) {
257 name = fnNames[i];
258 if (!this.isF(api[name])) {
259 return false;
260 }
261 fnLookup.set(name, true);
262 }
263
264 // check for properties on the API that are not listed in the array,
265 const keys = Object.getOwnPropertyNames(api);
266 if (keys.length === 0) {
267 return true;
268 }
269 // If the api is a class it will have a name,
270 // else it will just be called 'Object'
271 const apiObjectName: string = api.constructor.name;
272 if (apiObjectName === 'Object') {
273 Object.keys(api).forEach((key) => {
274 if (!fnLookup.get(key)) {
275 extraFound = true;
276 }
277 });
278 } else { // It is a class, so its functions will be in the child (prototype)
279 const pObj: Object = Object.getPrototypeOf(api);
280 for ( const key in Object.getOwnPropertyDescriptors(pObj) ) {
281 if (key === 'constructor') { // Filter out constructor
282 continue;
283 }
284 const value = Object.getOwnPropertyDescriptor(pObj, key);
285 // Only compare functions. Look for any not given in the map
286 if (this.isF(value.value) && !fnLookup.get(key)) {
287 extraFound = true;
288 }
289 }
290 }
291 return !extraFound;
292 }
293
294 /**
295 * Returns true if all names in the array are defined as functions
296 * on the given api object; false otherwise. This is a non-strict version
297 * that does not care about other properties on the api.
298 */
299 areFunctionsNonStrict(api, fnNames): boolean {
300 if (!this.isA(fnNames)) {
301 return false;
302 }
303 const n = fnNames.length;
304 let i;
305 let name;
306
307 for (i = 0; i < n; i++) {
308 name = fnNames[i];
309 if (!this.isF(api[name])) {
310 return false;
311 }
312 }
313 return true;
314 }
Sean Condon83fc39f2018-04-19 18:56:13 +0100315
316 /**
317 * Returns true if current browser determined to be a mobile device
318 */
319 isMobile() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100320 const ua = this.w.navigator.userAgent;
Sean Condon49e15be2018-05-16 16:58:29 +0100321 const patt = /iPhone|iPod|iPad|Silk|Android|BlackBerry|Opera Mini|IEMobile/;
Sean Condon83fc39f2018-04-19 18:56:13 +0100322 return patt.test(ua);
323 }
324
325 /**
326 * Returns true if the current browser determined to be Chrome
327 */
328 isChrome() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100329 const isChromium = (this.w as any).chrome;
330 const vendorName = this.w.navigator.vendor;
Sean Condon83fc39f2018-04-19 18:56:13 +0100331
Sean Condonfd6d11b2018-06-02 20:29:49 +0100332 const isOpera = this.w.navigator.userAgent.indexOf('OPR') > -1;
Sean Condon83fc39f2018-04-19 18:56:13 +0100333 return (isChromium !== null &&
334 isChromium !== undefined &&
335 vendorName === 'Google Inc.' &&
Sean Condon49e15be2018-05-16 16:58:29 +0100336 isOpera === false);
337 }
338
339 isChromeHeadless() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100340 const vendorName = this.w.navigator.vendor;
341 const headlessChrome = this.w.navigator.userAgent.indexOf('HeadlessChrome') > -1;
Sean Condon49e15be2018-05-16 16:58:29 +0100342
343 return (vendorName === 'Google Inc.' && headlessChrome === true);
Sean Condon83fc39f2018-04-19 18:56:13 +0100344 }
345
346 /**
347 * Returns true if the current browser determined to be Safari
348 */
349 isSafari() {
Sean Condonfd6d11b2018-06-02 20:29:49 +0100350 return (this.w.navigator.userAgent.indexOf('Safari') !== -1 &&
351 this.w.navigator.userAgent.indexOf('Chrome') === -1);
Sean Condon83fc39f2018-04-19 18:56:13 +0100352 }
353
354 /**
355 * Returns true if the current browser determined to be Firefox
356 */
357 isFirefox() {
358 return typeof InstallTrigger !== 'undefined';
359 }
360
361 /**
Sean Condonfd6d11b2018-06-02 20:29:49 +0100362 * search through an array of objects, looking for the one with the
363 * tagged property matching the given key. tag defaults to 'id'.
364 * returns the index of the matching object, or -1 for no match.
365 */
366 find(key: string, array: Object[], tag: string = 'id'): number {
367 let idx: number;
368 const n: number = array.length;
369
370 for (idx = 0 ; idx < n; idx++) {
371 const d: Object = array[idx];
372 if (d[tag] === key) {
373 return idx;
374 }
375 }
376 return -1;
377 }
378
379 /**
380 * search through array to find (the first occurrence of) item,
381 * returning its index if found; otherwise returning -1.
382 */
383 inArray(item: any, array: any[]): number {
384 if (this.isA(array)) {
385 for (let i = 0; i < array.length; i++) {
386 if (array[i] === item) {
387 return i;
388 }
389 }
390 }
391 return -1;
392 }
393
394 /**
395 * remove (the first occurrence of) the specified item from the given
396 * array, if any. Return true if the removal was made; false otherwise.
397 */
398 removeFromArray(item: any, array: any[]): boolean {
399 const i: number = this.inArray(item, array);
400 if (i >= 0) {
401 array.splice(i, 1);
402 return true;
403 }
404 return false;
405 }
406
407 /**
408 * return true if the object is empty, return false otherwise
409 */
410 isEmptyObject(obj: Object): boolean {
411 for (const key in obj) {
412 if (true) { return false; }
413 }
414 return true;
415 }
416
417 /**
Sean Condon83fc39f2018-04-19 18:56:13 +0100418 * Return the given string with the first character capitalized.
419 */
Sean Condonfd6d11b2018-06-02 20:29:49 +0100420 cap(s: string): string {
Sean Condon83fc39f2018-04-19 18:56:13 +0100421 return s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s;
422 }
423
424 /**
Sean Condonfd6d11b2018-06-02 20:29:49 +0100425 * return the parameter without a px suffix
426 */
427 noPx(num: string): number {
428 return Number(num.replace(/px$/, ''));
429 }
430
431 /**
432 * return an element's given style property without px suffix
433 */
434 noPxStyle(elem: any, prop: string): number {
435 return Number(elem.style(prop).replace(/px$/, ''));
436 }
437
438 /**
439 * Return true if a str ends with suffix
440 */
441 endsWith(str: string, suffix: string) {
442 return str.indexOf(suffix, str.length - suffix.length) !== -1;
443 }
444
445 /**
Sean Condon83fc39f2018-04-19 18:56:13 +0100446 * output debug message to console, if debug tag set...
447 * e.g. fs.debug('mytag', arg1, arg2, ...)
448 */
449 debug(tag, ...args) {
450 if (this.debugFlags.get(tag)) {
451 this.log.debug(tag, args.join());
452 }
453 }
454
Sean Condonfd6d11b2018-06-02 20:29:49 +0100455 private parseDebugFlags(dbgstr: string): void {
Sean Condon49e15be2018-05-16 16:58:29 +0100456 const bits = dbgstr ? dbgstr.split(',') : [];
457 bits.forEach((key) => {
Sean Condon83fc39f2018-04-19 18:56:13 +0100458 this.debugFlags.set(key, true);
459 });
460 this.log.debug('Debug flags:', dbgstr);
461 }
462
463 /**
464 * Return true if the given debug flag was specified in the query params
465 */
466 debugOn(tag: string): boolean {
467 return this.debugFlags.get(tag);
468 }
469
Sean Condonfd6d11b2018-06-02 20:29:49 +0100470
471
472 // -----------------------------------------------------------------
473 // The next section deals with sanitizing external strings destined
474 // to be loaded via a .html() function call.
475 //
476 // See definition of matcher, evillist and whitelist at the top of this file
477
478 /*
479 * Returns true if the tag is in the evil list, (and is not an end-tag)
480 */
481 inEvilList(tag: any): boolean {
482 return (evillist.indexOf(tag.name) !== -1 && tag.full.indexOf('/') === -1);
483 }
484
485 /*
486 * Returns an array of Matches of matcher in html
487 */
488 analyze(html: string): Match[] {
489 const matches: Match[] = [];
490 let match;
491
492 // extract all tags
493 while ((match = matcher.exec(html)) !== null) {
494 matches.push({
495 full: match[0],
496 name: match[1],
497 // NOTE: ignoring attributes {match[2].split(' ')} for now
498 });
499 }
500
501 return matches;
502 }
503
504 /*
505 * Returns a cleaned version of html
506 */
507 sanitize(html: string): string {
508 const matches: Match[] = this.analyze(html);
509
510 // completely obliterate evil tags and their contents...
511 evillist.forEach((tag) => {
512 const re = new RegExp('<' + tag + '(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/' + tag + '>', 'gim');
513 html = html.replace(re, '');
514 });
515
516 // filter out all but white-listed tags and end-tags
517 matches.forEach((tag) => {
518 if (whitelist.indexOf(tag.name) === -1) {
519 html = html.replace(tag.full, '');
520 if (this.inEvilList(tag)) {
521 this.log.warn('Unsanitary HTML input -- ' +
522 tag.full + ' detected!');
523 }
524 }
525 });
526
527 // TODO: consider encoding HTML entities, e.g. '&' -> '&amp;'
528
529 return html;
530 }
531
Sean Condon83fc39f2018-04-19 18:56:13 +0100532}