All files / src/app/shared/services/user-agent user-agent.service.ts

76.74% Statements 33/43
64.28% Branches 18/28
87.5% Functions 7/8
82.5% Lines 33/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121                    1x   1x                                                 11x   11x 80x 80x 80x   80x   80x               11x     11x   2x       11x       11x       11x   99x 99x                   43x 86x     86x 86x     86x     43x 43x 43x     43x 43x 6x   43x 6x   43x 21x   22x                    
import { Injectable } from '@angular/core';
import { coerce, gte, satisfies } from 'semver';
import { ResolvedUserAgent, resolveUserAgent } from 'browserslist-useragent';
import UAParser from 'ua-parser-js';
// eslint-disable-next-line import/no-relative-packages
import browsersJson from '../../../../../../definitions/browsers.json';
 
@Injectable({
  providedIn: 'root'
})
export class UserAgentService {
  // see https://github.com/ai/browserslist#browsers
  static browserNameMap: { [browserlistId: string]: string } = {
    bb: 'BlackBerry',
    and_chr: 'Chrome',
    ChromeAndroid: 'Chrome',
    FirefoxAndroid: 'Firefox',
    ff: 'Firefox',
    ie_mob: 'ExplorerMobile',
    ie: 'Explorer',
    and_ff: 'Firefox',
    ios_saf: 'iOS',
    op_mini: 'OperaMini',
    op_mob: 'OperaMobile',
    and_qq: 'QQAndroid',
    and_uc: 'UCAndroid'
  };
 
  // we can't use matchesUA from browserslist-useragent because it expects a set of browserslist-queries not an already
  // parsed list of supported browsers. We parse our list once every release to make it more efficient.
  // Apart from that, this function is very similar and inspired by matchesUA.
  // TODO wrap put every usage of userAgent and UAparser into this service
  static userAgentMatches(
    userAgent: ResolvedUserAgent,
    browsersList: string[] = browsersJson.browsers,
    allowHigherVersions: boolean = true
  ): boolean {
    const parsedBrowsers = UserAgentService.parseBrowsersList(browsersList);
 
    return parsedBrowsers.some(browser => {
      Iif (!userAgent.family) return false;
      Iif (!userAgent.version) return false;
      Iif (!browser.version) return false;
 
      const allowHigher = allowHigherVersions ? 'major' : null;
 
      return (browser.family.toLowerCase() === userAgent.family.toLocaleLowerCase() &&
        UserAgentService.versionSatisfies(userAgent.version, browser.version, allowHigher)
      );
    });
  }
 
  // inspired by resolveUserAgent from browserslist-useragent which is not publicly exported unfortunately
  static resolveUserAgent(UAstring: string = window.navigator.userAgent): ResolvedUserAgent {
    const parsedUA = UAParser(UAstring).browser;
 
    // https://bugzilla.mozilla.org/show_bug.cgi?id=1805967
    if ((parsedUA.name === 'Firefox') && UAstring.match(/rv:109/)) {
      // eslint-disable-next-line no-param-reassign
      UAstring = UAstring.replace(/rv:109/, `rv:${parsedUA.version}`);
    }
 
    // https://news.ycombinator.com/item?id=20030340
    Iif ((parsedUA.name === 'Edge') && UAstring.match(/Edg\//)) {
      // eslint-disable-next-line no-param-reassign
      UAstring = UAstring.replace(/Edg\//, 'Edge/');
    }
    return resolveUserAgent(UAstring);
  }
 
  static parseBrowsersList(simpleBrowserList: string[]): { family: string; version: string }[] {
    return simpleBrowserList
      .map(browser => {
        const [family, version] = browser.split(' ');
        return { family: UserAgentService.browserNameMap[family] ?? family, version };
      });
  }
 
  // inspired by compareBrowserSemvers from browserslist-useragent which is not publicly exported unfortunately
  static versionSatisfies(
    testSemver: string,
    constraintSemver: string,
    allowHigher: 'patch' | 'minor' | 'major' | null = null
  ): boolean {
    const semverify = (version: string) => {
      Iif (!version) {
        return null;
      }
      const coerced = coerce(version, { loose: true });
      Iif (!coerced) {
        return null;
      }
      return coerced.version;
    };
 
    const semverifiedA = semverify(testSemver);
    const semverifiedB = semverify(constraintSemver);
    Iif (!semverifiedA || !semverifiedB) {
      return false;
    }
    let referenceVersion = semverifiedB;
    if (allowHigher === 'patch') {
      referenceVersion = `~${semverifiedB}`;
    }
    if (allowHigher === 'minor') {
      referenceVersion = `^${semverifiedB}`;
    }
    if (allowHigher === 'major') {
      return gte(semverifiedA, semverifiedB);
    }
    return satisfies(semverifiedA, referenceVersion);
  }
 
  static outputWithOs(UAstring: string = window.navigator.userAgent): string {
    const browser = this.resolveUserAgent(UAstring);
    const os = UAParser(UAstring).os;
 
    return `${os.name}/${os.version} ${browser.family}/${browser.version}`;
  }
}