Quellcode durchsuchen

Merge pull request #569 from threema-ch/safari10

Refactor browser detection
Danilo Bargen vor 7 Jahren
Ursprung
Commit
70d78b37d9

+ 1 - 1
src/app.ts

@@ -73,7 +73,7 @@ angular.module('3ema', [
     FF: 50,
     CHROME: 45,
     OPERA: 32,
-    SAFARI: 11,
+    SAFARI: 10,
 })
 
 // Set default route

+ 2 - 2
src/directives/compose_area.ts

@@ -367,7 +367,7 @@ export default [
                             };
 
                             // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1240259
-                            if (browserService.getBrowser().firefox) {
+                            if (browserService.getBrowser().isFirefox(false)) {
                                 if (fileMessageData.name.endsWith('.ogg') && fileMessageData.fileType === 'video/ogg') {
                                     fileMessageData.fileType = 'audio/ogg';
                                 }
@@ -634,7 +634,7 @@ export default [
                         span.setAttribute('contenteditable', false);
                     }
 
-                    if (browserService.getBrowser().firefox) {
+                    if (browserService.getBrowser().isFirefox(false)) {
                         // disable object resizing is the only way to disable resizing of
                         // emoji (contenteditable must be true, otherwise the emoji can not
                         // be removed with backspace (in FF))

+ 99 - 0
src/helpers/browser_info.ts

@@ -0,0 +1,99 @@
+/**
+ * This file is part of Threema Web.
+ *
+ * Threema Web is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+export class BrowserInfo {
+    private userAgent: string;
+
+    public readonly name: threema.BrowserName | null;
+    public readonly version: number | null;
+    public readonly mobile: boolean;
+
+    constructor(
+        userAgent: string,
+        name: threema.BrowserName | null,
+        version: number | null,
+        mobile: boolean = false,
+    ) {
+        this.userAgent = userAgent;
+        this.name = name;
+        this.version = version;
+        this.mobile = mobile;
+    }
+
+    public wasDetermined(): boolean {
+        return this.name !== null && this.version !== null;
+    }
+
+    public description(): string {
+        if (this.name === null) {
+            return 'Unknown';
+        }
+        let description = '';
+        switch (this.name) {
+            case threema.BrowserName.Chrome:
+                description = 'Chrome ' + this.version;
+                break;
+            case threema.BrowserName.ChromeIos:
+                description = 'Chrome (iOS) ' + this.version;
+                break;
+            case threema.BrowserName.Firefox:
+                description = 'Firefox ' + this.version;
+                break;
+            case threema.BrowserName.FirefoxIos:
+                description = 'Firefox (iOS) ' + this.version;
+                break;
+            case threema.BrowserName.Edge:
+                description = 'Edge ' + this.version;
+                break;
+            case threema.BrowserName.InternetExplorer:
+                description = 'Internet Explorer ' + this.version;
+                break;
+            case threema.BrowserName.Opera:
+                description = 'Opera ' + this.version;
+                break;
+            case threema.BrowserName.Safari:
+                description = 'Safari ' + this.version;
+                break;
+        }
+        if (this.mobile) {
+            description += ' [Mobile]';
+        }
+        return description;
+    }
+
+    /**
+     * Return whether the current browser supports the WebRTC task or not.
+     */
+    public supportsWebrtcTask(): boolean {
+        switch (this.name) {
+            case threema.BrowserName.Safari:
+            case threema.BrowserName.FirefoxIos:
+            case threema.BrowserName.ChromeIos:
+                return false;
+            default:
+                return true;
+        }
+    }
+
+    public isFirefox(requireVersion: boolean): boolean {
+        return this.name === threema.BrowserName.Firefox && (!requireVersion || this.version !== null);
+    }
+
+    public isSafari(requireVersion: boolean): boolean {
+        return this.name === threema.BrowserName.Safari && (!requireVersion || this.version !== null);
+    }
+}

+ 8 - 7
src/partials/welcome.ts

@@ -25,6 +25,7 @@ import {
     StateService as UiStateService,
 } from '@uirouter/angularjs';
 
+import {BrowserInfo} from '../helpers/browser_info';
 import {BrowserService} from '../services/browser';
 import {ControllerService} from '../services/controller';
 import {TrustedKeyStoreService} from '../services/keystore';
@@ -89,7 +90,7 @@ class WelcomeController {
     private password: string = '';
     private formLocked: boolean = false;
     private pleaseUpdateAppMsg: string = null;
-    private browser: threema.BrowserInfo;
+    private browser: BrowserInfo;
     private browserWarningShown: boolean = false;
 
     public static $inject = [
@@ -132,26 +133,26 @@ class WelcomeController {
         // Determine whether browser warning should be shown
         this.browser = browserService.getBrowser();
         const version = this.browser.version;
-        $log.debug('Detected browser:', this.browser.textInfo);
-        if (version === undefined) {
+        $log.debug('Detected browser:', this.browser.description());
+        if (!this.browser.wasDetermined()) {
             $log.warn('Could not determine browser version');
             this.showBrowserWarning();
-        } else if (this.browser.chrome === true) {
+        } else if (this.browser.name === threema.BrowserName.Chrome) {
             if (version < minVersions.CHROME) {
                 $log.warn('Chrome is too old (' + version + ' < ' + minVersions.CHROME + ')');
                 this.showBrowserWarning();
             }
-        } else if (this.browser.firefox === true) {
+        } else if (this.browser.name === threema.BrowserName.Firefox) {
             if (version < minVersions.FF) {
                 $log.warn('Firefox is too old (' + version + ' < ' + minVersions.FF + ')');
                 this.showBrowserWarning();
             }
-        } else if (this.browser.opera === true) {
+        } else if (this.browser.name === threema.BrowserName.Opera) {
             if (version < minVersions.OPERA) {
                 $log.warn('Opera is too old (' + version + ' < ' + minVersions.OPERA + ')');
                 this.showBrowserWarning();
             }
-        } else if (this.browser.safari === true) {
+        } else if (this.browser.name === threema.BrowserName.Safari) {
             if (version < minVersions.SAFARI) {
                 $log.warn('Safari is too old (' + version + ' < ' + minVersions.SAFARI + ')');
                 this.showBrowserWarning();

+ 51 - 41
src/services/browser.ts

@@ -15,12 +15,14 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
+import {BrowserInfo} from '../helpers/browser_info';
+
 import BrowserName = threema.BrowserName;
 
 export class BrowserService {
     private logTag: string = '[BrowserService]';
 
-    private browser: threema.BrowserInfo;
+    private browser: BrowserInfo;
     private $log: ng.ILogService;
     private $window: ng.IWindowService;
     private isPageVisible = true;
@@ -86,33 +88,39 @@ export class BrowserService {
         }
     }
 
-    public getBrowser(): threema.BrowserInfo {
+    public getBrowser(): BrowserInfo {
         if (this.browser === undefined) {
-            this.browser = {
+            const browser = {
                 chrome: false,
+                chromeIos: false,
                 firefox: false,
+                firefoxIos: false,
                 ie: false,
                 edge: false,
                 opera: false,
                 safari: false,
-            } as threema.BrowserInfo;
+            };
 
             const uagent = this.$window.navigator.userAgent.toLowerCase();
 
-            this.browser.chrome  = /webkit/.test(uagent)  && /chrome/.test(uagent) && !/edge/.test(uagent);
-            this.browser.firefox = /mozilla/.test(uagent) && /firefox/.test(uagent);
-            this.browser.ie      = (/msie/.test(uagent) || /trident/.test(uagent)) && !/edge/.test(uagent);
-            this.browser.edge    = /edge/.test(uagent);
-            this.browser.safari  = /safari/.test(uagent)  && /applewebkit/.test(uagent) && !/chrome/.test(uagent);
-            this.browser.opera   = /mozilla/.test(uagent) && /applewebkit/.test(uagent)
+            browser.chrome = /webkit/.test(uagent) && /chrome/.test(uagent) && !/edge/.test(uagent);
+            browser.chromeIos = /mozilla/.test(uagent) && /crios/.test(uagent);
+            browser.firefox = /mozilla/.test(uagent) && /firefox/.test(uagent);
+            browser.firefoxIos = /mozilla/.test(uagent) && /fxios/.test(uagent);
+            browser.ie = (/msie/.test(uagent) || /trident/.test(uagent)) && !/edge/.test(uagent);
+            browser.edge = /edge/.test(uagent);
+            browser.safari = /safari/.test(uagent) && /applewebkit/.test(uagent)
+                          && !/chrome/.test(uagent) && !/fxios/.test(uagent) && !/crios/.test(uagent);
+            browser.opera = /mozilla/.test(uagent) && /applewebkit/.test(uagent)
                 && /chrome/.test(uagent) && /safari/.test(uagent) && /opr/.test(uagent);
 
-            if (this.browser.opera && this.browser.chrome) {
-                this.browser.chrome = false;
+            if (browser.opera && browser.chrome) {
+                browser.chrome = false;
             }
 
-            for (const x in this.browser) {
-                if (this.browser[x]) {
+            let version = null;
+            for (const x in browser) {
+                if (browser[x]) {
                     let b;
                     if (x === 'ie') {
                         b = 'msie';
@@ -120,6 +128,10 @@ export class BrowserService {
                         b = 'edge';
                     } else if (x === 'opera') {
                         b = 'opr';
+                    } else if (x === 'firefoxIos') {
+                        b = 'fxios';
+                    } else if (x === 'chromeIos') {
+                        b = 'crios';
                     } else if (x === 'safari') {
                         b = 'version';
                     } else {
@@ -127,43 +139,44 @@ export class BrowserService {
                     }
                     let match = uagent.match(new RegExp('(' + b + ')( |\/)([0-9]+)'));
 
-                    let version;
+                    let versionString;
                     if (match) {
-                        version = match[3];
+                        versionString = match[3];
                     } else {
                         match = uagent.match(new RegExp('rv:([0-9]+)'));
-                        version = match ? match[1] : '';
+                        versionString = match ? match[1] : '';
                     }
-                    const versionInt: number = parseInt(match[3], 10);
-                    this.browser.version = isNaN(versionInt) ? undefined : versionInt;
+                    const versionInt: number = parseInt(versionString, 10);
+                    version = isNaN(versionInt) ? undefined : versionInt;
 
                     break;
                 }
             }
 
-            if (this.browser.chrome) {
-                this.browser.name = BrowserName.Chrome;
-                this.browser.textInfo = 'Chrome ' + this.browser.version;
+            if (browser.chrome) {
+                this.browser = new BrowserInfo(uagent, BrowserName.Chrome, version);
+            }
+            if (browser.chromeIos) {
+                this.browser = new BrowserInfo(uagent, BrowserName.ChromeIos, version, true);
+            }
+            if (browser.firefox) {
+                this.browser = new BrowserInfo(uagent, BrowserName.Firefox, version);
             }
-            if (this.browser.firefox) {
-                this.browser.name = BrowserName.Firefox;
-                this.browser.textInfo = 'Firefox ' + this.browser.version;
+            if (browser.firefoxIos) {
+                this.browser = new BrowserInfo(uagent, BrowserName.FirefoxIos, version, true);
             }
-            if (this.browser.ie) {
-                this.browser.name = BrowserName.InternetExplorer;
-                this.browser.textInfo = 'Internet Explorer ' + this.browser.version;
+            if (browser.ie) {
+                this.browser = new BrowserInfo(uagent, BrowserName.InternetExplorer, version);
             }
-            if (this.browser.edge) {
-                this.browser.name = BrowserName.Edge;
-                this.browser.textInfo = 'Edge ' + this.browser.version;
+            if (browser.edge) {
+                this.browser = new BrowserInfo(uagent, BrowserName.Edge, version);
             }
-            if (this.browser.safari) {
-                this.browser.name = BrowserName.Safari;
-                this.browser.textInfo = 'Safari ' + this.browser.version;
+            if (browser.safari) {
+                const mobile = /mobile/.test(uagent);
+                this.browser = new BrowserInfo(uagent, BrowserName.Safari, version, mobile);
             }
-            if (this.browser.opera) {
-                this.browser.name = BrowserName.Opera;
-                this.browser.textInfo = 'Opera ' + this.browser.version;
+            if (browser.opera) {
+                this.browser = new BrowserInfo(uagent, BrowserName.Opera, version);
             }
         }
 
@@ -181,10 +194,7 @@ export class BrowserService {
         if (this.browser === undefined) {
             this.getBrowser();
         }
-        if (this.browser.safari) {
-            return false;
-        }
-        return true;
+        return this.browser.supportsWebrtcTask();
     }
 
     /**

+ 3 - 3
src/services/webclient.ts

@@ -326,7 +326,7 @@ export class WebClientService {
         this.stateService.reset();
 
         // Create WebRTC task instance
-        const maxPacketSize = this.browserService.getBrowser().firefox ? 16384 : 65536;
+        const maxPacketSize = this.browserService.getBrowser().isFirefox(false) ? 16384 : 65536;
         this.webrtcTask = new saltyrtcTaskWebrtc.WebRTCTask(true, maxPacketSize);
 
         // Create Relayed Data task instance
@@ -437,12 +437,12 @@ export class WebClientService {
                 const browser = this.browserService.getBrowser();
 
                 // Firefox <53 does not yet support TLS. Skip it, to save allocations.
-                if (browser.firefox && browser.version && browser.version < 53) {
+                if (browser.isFirefox(true) && browser.version < 53) {
                     this.skipIceTls();
                 }
 
                 // Safari does not support our dual-stack TURN servers.
-                if (browser.safari) {
+                if (browser.isSafari(false)) {
                     this.skipIceDs();
                 }
 

+ 5 - 0
src/threema.d.ts

@@ -504,7 +504,9 @@ declare namespace threema {
 
     const enum BrowserName {
         Chrome = 'chrome',
+        ChromeIos = 'chromeIos',
         Firefox = 'firefox',
+        FirefoxIos = 'firefoxIos',
         InternetExplorer = 'ie',
         Edge = 'edge',
         Opera = 'opera',
@@ -513,12 +515,15 @@ declare namespace threema {
 
     interface BrowserInfo {
         chrome: boolean;
+        chromeIos: boolean;
         firefox: boolean;
+        firefoxIos: boolean;
         ie: boolean;
         edge: boolean;
         opera: boolean;
         safari: boolean;
         name?: BrowserName;
+        mobile?: boolean;
         version?: number;
         textInfo?: string;
     }

+ 65 - 64
tests/service/browser.js

@@ -1,3 +1,14 @@
+const BrowserName = {
+    Chrome: 'chrome',
+    ChromeIos: 'chromeIos',
+    Firefox: 'firefox',
+    FirefoxIos: 'firefoxIos',
+    InternetExplorer: 'ie',
+    Edge: 'edge',
+    Opera: 'opera',
+    Safari: 'safari',
+};
+
 describe('BrowserService', function() {
 
     function testUserAgent(agent) {
@@ -18,120 +29,110 @@ describe('BrowserService', function() {
         const ua = 'Mozilla/5.0 (X11; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0';
         const service = testUserAgent(ua);
         const browser = service.getBrowser();
-        expect(browser.chrome).toBe(false);
-        expect(browser.firefox).toBe(true);
-        expect(browser.ie).toBe(false);
-        expect(browser.edge).toBe(false);
-        expect(browser.opera).toBe(false);
-        expect(browser.safari).toBe(false);
-        expect(browser.name).toEqual('firefox');
+        expect(browser.name).toEqual(BrowserName.Firefox);
         expect(browser.version).toEqual(59);
-        expect(browser.textInfo).toEqual('Firefox 59');
+        expect(browser.mobile).toBe(false);
+        expect(browser.description()).toEqual('Firefox 59');
+    });
+
+    it('firefoxIosMobile', () => {
+        const ua = 'Mozilla/5.0 (iPad; CPU OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) FxiOS/8.3b5826 Mobile/14A403 Safari/602.1.50';
+        const service = testUserAgent(ua);
+        const browser = service.getBrowser();
+        expect(browser.name).toBe(BrowserName.FirefoxIos);
+        expect(browser.version).toEqual(8);
+        expect(browser.mobile).toBe(true);
+        expect(browser.description()).toEqual('Firefox (iOS) 8 [Mobile]');
     });
 
     it('chrome', () => {
         const ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36';
         const service = testUserAgent(ua);
         const browser = service.getBrowser();
-        expect(browser.chrome).toBe(true);
-        expect(browser.firefox).toBe(false);
-        expect(browser.ie).toBe(false);
-        expect(browser.edge).toBe(false);
-        expect(browser.opera).toBe(false);
-        expect(browser.safari).toBe(false);
-        expect(browser.name).toEqual('chrome');
+        expect(browser.name).toBe(BrowserName.Chrome);
         expect(browser.version).toEqual(65);
-        expect(browser.textInfo).toEqual('Chrome 65');
+        expect(browser.mobile).toBe(false);
+        expect(browser.description()).toEqual('Chrome 65');
+    });
+
+    it('chromeIosMobile', () => {
+        const ua = 'Mozilla/5.0 (iPad; CPU OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/68.0.3440.83 Mobile/14A403 Safari/602.1';
+        const service = testUserAgent(ua);
+        const browser = service.getBrowser();
+        expect(browser.name).toBe(BrowserName.ChromeIos);
+        expect(browser.version).toEqual(68);
+        expect(browser.mobile).toBe(true);
+        expect(browser.description()).toEqual('Chrome (iOS) 68 [Mobile]');
     });
 
     it('ie9', () => {
         const ua = 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0)';
         const service = testUserAgent(ua);
         const browser = service.getBrowser();
-        expect(browser.chrome).toBe(false);
-        expect(browser.firefox).toBe(false);
-        expect(browser.ie).toBe(true);
-        expect(browser.edge).toBe(false);
-        expect(browser.opera).toBe(false);
-        expect(browser.safari).toBe(false);
-        expect(browser.name).toEqual('ie');
+        expect(browser.name).toBe(BrowserName.InternetExplorer);
         expect(browser.version).toEqual(9);
-        expect(browser.textInfo).toEqual('Internet Explorer 9');
+        expect(browser.mobile).toBe(false);
+        expect(browser.description()).toEqual('Internet Explorer 9');
     });
 
     it('ie11', () => {
         const ua = 'Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko';
         const service = testUserAgent(ua);
         const browser = service.getBrowser();
-        expect(browser.chrome).toBe(false);
-        expect(browser.firefox).toBe(false);
-        expect(browser.ie).toBe(true);
-        expect(browser.edge).toBe(false);
-        expect(browser.opera).toBe(false);
-        expect(browser.safari).toBe(false);
-        expect(browser.name).toEqual('ie');
+        expect(browser.name).toBe(BrowserName.InternetExplorer);
         expect(browser.version).toEqual(11);
-        expect(browser.textInfo).toEqual('Internet Explorer 11');
+        expect(browser.mobile).toBe(false);
+        expect(browser.description()).toEqual('Internet Explorer 11');
     });
 
     it('edge12', () => {
         const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246';
         const service = testUserAgent(ua);
         const browser = service.getBrowser();
-        expect(browser.chrome).toBe(false);
-        expect(browser.firefox).toBe(false);
-        expect(browser.ie).toBe(false);
-        expect(browser.edge).toBe(true);
-        expect(browser.opera).toBe(false);
-        expect(browser.safari).toBe(false);
-        expect(browser.name).toEqual('edge');
+        expect(browser.name).toBe(BrowserName.Edge);
         expect(browser.version).toEqual(12);
-        expect(browser.textInfo).toEqual('Edge 12');
+        expect(browser.mobile).toBe(false);
+        expect(browser.description()).toEqual('Edge 12');
     });
 
     it('opera', () => {
         const ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36 OPR/51.0.2830.55';
         const service = testUserAgent(ua);
         const browser = service.getBrowser();
-        expect(browser.chrome).toBe(false);
-        expect(browser.firefox).toBe(false);
-        expect(browser.ie).toBe(false);
-        expect(browser.edge).toBe(false);
-        expect(browser.opera).toBe(true);
-        expect(browser.safari).toBe(false);
-        expect(browser.name).toEqual('opera');
+        expect(browser.name).toBe(BrowserName.Opera);
         expect(browser.version).toEqual(51);
-        expect(browser.textInfo).toEqual('Opera 51');
+        expect(browser.mobile).toBe(false);
+        expect(browser.description()).toEqual('Opera 51');
     });
 
     it('safari7', () => {
         const ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A';
         const service = testUserAgent(ua);
         const browser = service.getBrowser();
-        expect(browser.chrome).toBe(false);
-        expect(browser.firefox).toBe(false);
-        expect(browser.ie).toBe(false);
-        expect(browser.edge).toBe(false);
-        expect(browser.opera).toBe(false);
-        expect(browser.safari).toBe(true);
-        expect(browser.name).toEqual('safari');
+        expect(browser.name).toBe(BrowserName.Safari);
         expect(browser.version).toEqual(7);
-        expect(browser.textInfo).toEqual('Safari 7');
+        expect(browser.mobile).toBe(false);
+        expect(browser.description()).toEqual('Safari 7');
     });
 
     it('safari11', () => {
         const ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0.3 Safari/604.5.6';
         const service = testUserAgent(ua);
         const browser = service.getBrowser();
-        expect(browser.chrome).toBe(false);
-        expect(browser.firefox).toBe(false);
-        expect(browser.ie).toBe(false);
-        expect(browser.edge).toBe(false);
-        expect(browser.opera).toBe(false);
-        expect(browser.safari).toBe(true);
-        expect(browser.name).toEqual('safari');
+        expect(browser.name).toBe(BrowserName.Safari);
         expect(browser.version).toEqual(11);
-        expect(browser.textInfo).toEqual('Safari 11');
+        expect(browser.mobile).toBe(false);
+        expect(browser.description()).toEqual('Safari 11');
+    });
+
+    it('safari10Mobile', () => {
+        const ua = 'Mozilla/5.0 (iPad; CPU OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A403 Safari/602.1';
+        const service = testUserAgent(ua);
+        const browser = service.getBrowser();
+        expect(browser.name).toBe(BrowserName.Safari);
+        expect(browser.version).toEqual(10);
+        expect(browser.mobile).toBe(true);
+        expect(browser.description()).toEqual('Safari 10 [Mobile]');
     });
 
 });