From 9387a3f394074a6ff182634a8f015584a1599e46 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 11 Jun 2020 14:39:14 +0200 Subject: [PATCH] Catch unsupported browsers unspoorted browsers trying to access the login mask will be forwarded to an info page. The info page shows that the browser is not suppoted and hints the smallest supported version of their current browser. As it works best and might prevent some support calls, I added an hint for chrome as the favored browser by OpenSlides (debateable) To update/downgrade the supported versions, simply edit the enum in the service. If we cannot detect the browser, we assume it was supported. --- client/package.json | 1 + client/src/app/app-routing.module.ts | 4 +- .../login-mask/login-mask.component.ts | 21 ++++++- .../login-wrapper.component.html | 4 +- .../unsupported-browser.component.html | 37 +++++++++++ .../unsupported-browser.component.spec.ts | 27 ++++++++ .../unsupported-browser.component.ts | 28 +++++++++ client/src/app/site/login/login.module.ts | 8 ++- .../services/browser-support.service.spec.ts | 16 +++++ .../login/services/browser-support.service.ts | 63 +++++++++++++++++++ 10 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.html create mode 100644 client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.spec.ts create mode 100644 client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.ts create mode 100644 client/src/app/site/login/services/browser-support.service.spec.ts create mode 100644 client/src/app/site/login/services/browser-support.service.ts diff --git a/client/package.json b/client/package.json index 1b4962f69..9ea16aab0 100644 --- a/client/package.json +++ b/client/package.json @@ -63,6 +63,7 @@ "moment": "^2.24.0", "ng2-charts": "^2.3.0", "ng2-pdf-viewer": "^6.1.2", + "ngx-device-detector": "^1.4.4", "ngx-file-drop": "^8.0.8", "ngx-mat-select-search": "^2.1.2", "ngx-material-timepicker": "^5.5.1", diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index d823ee84b..0a3a3fa89 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -7,6 +7,7 @@ import { LoginPrivacyPolicyComponent } from './site/login/components/login-priva import { LoginWrapperComponent } from './site/login/components/login-wrapper/login-wrapper.component'; import { ResetPasswordConfirmComponent } from './site/login/components/reset-password-confirm/reset-password-confirm.component'; import { ResetPasswordComponent } from './site/login/components/reset-password/reset-password.component'; +import { UnsupportedBrowserComponent } from './site/login/components/unsupported-browser/unsupported-browser.component'; /** * Global app routing @@ -20,7 +21,8 @@ const routes: Routes = [ { path: 'reset-password', component: ResetPasswordComponent }, { path: 'reset-password-confirm', component: ResetPasswordConfirmComponent }, { path: 'legalnotice', component: LoginLegalNoticeComponent }, - { path: 'privacypolicy', component: LoginPrivacyPolicyComponent } + { path: 'privacypolicy', component: LoginPrivacyPolicyComponent }, + { path: 'unsupported-browser', component: UnsupportedBrowserComponent } ] }, { diff --git a/client/src/app/site/login/components/login-mask/login-mask.component.ts b/client/src/app/site/login/components/login-mask/login-mask.component.ts index 0df52349d..bb66fb0bf 100644 --- a/client/src/app/site/login/components/login-mask/login-mask.component.ts +++ b/client/src/app/site/login/components/login-mask/login-mask.component.ts @@ -6,6 +6,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { AuthService } from 'app/core/core-services/auth.service'; import { OperatorService } from 'app/core/core-services/operator.service'; @@ -14,6 +15,7 @@ import { OverlayService } from 'app/core/ui-services/overlay.service'; import { UserAuthType } from 'app/shared/models/users/user'; import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher'; import { BaseViewComponent } from 'app/site/base/base-view'; +import { BrowserSupportService } from '../../services/browser-support.service'; /** * Login mask component. @@ -31,6 +33,8 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD */ public hide: boolean; + private checkBrowser = true; + /** * Reference to the SnackBarEntry for the installation notice send by the server. */ @@ -82,7 +86,8 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD private route: ActivatedRoute, private formBuilder: FormBuilder, private loginDataService: LoginDataService, - private overlayService: OverlayService + private overlayService: OverlayService, + private browserSupport: BrowserSupportService ) { super(title, translate, matSnackBar); // Hide the spinner if the user is at `login-mask` @@ -113,6 +118,14 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD this.authService.redirectUser(user.id); } }); + + this.route.queryParams.pipe(filter(params => params.checkBrowser)).subscribe(params => { + this.checkBrowser = params.checkBrowser === 'true'; + }); + + if (this.checkBrowser) { + this.checkDevice(); + } } /** @@ -122,6 +135,12 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD this.clearOperatorSubscription(); } + private checkDevice(): void { + if (!this.browserSupport.isBrowserSupported()) { + this.router.navigate(['./unsupported-browser'], { relativeTo: this.route }); + } + } + /** * Clears the subscription to the operator. */ diff --git a/client/src/app/site/login/components/login-wrapper/login-wrapper.component.html b/client/src/app/site/login/components/login-wrapper/login-wrapper.component.html index 4bc549666..6c493032f 100644 --- a/client/src/app/site/login/components/login-wrapper/login-wrapper.component.html +++ b/client/src/app/site/login/components/login-wrapper/login-wrapper.component.html @@ -1,7 +1,9 @@
diff --git a/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.html b/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.html new file mode 100644 index 000000000..618bc6ed8 --- /dev/null +++ b/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.html @@ -0,0 +1,37 @@ +
+

+ {{ 'Your browser is not supported by OpenSlides' | translate }} +

+

+ {{ 'Congratuations! Your browser is supported by OpenSlides' | translate }} +

+ + +
+ + {{ 'Your browser' | translate }}: {{ currentBrowser }} ({{ 'version' | translate }}: + {{ browserVersion }}) + +
+ {{ 'Minimal required version for your browser' | translate }}: {{ supportedVersion }} +
+ +
+ {{ + 'Please update your browser or contact your system administration to have your browser updated for you.' + | translate + }} +
+ +
+ + {{ 'OpenSlides recommends:' | translate }} + + +
+
+
diff --git a/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.spec.ts b/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.spec.ts new file mode 100644 index 000000000..9057c1865 --- /dev/null +++ b/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.spec.ts @@ -0,0 +1,27 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { E2EImportsModule } from 'e2e-imports.module'; + +import { UnsupportedBrowserComponent } from './unsupported-browser.component'; + +describe('UnsupportedBrowserComponent', () => { + let component: UnsupportedBrowserComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + declarations: [UnsupportedBrowserComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UnsupportedBrowserComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.ts b/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.ts new file mode 100644 index 000000000..f4dfb0e11 --- /dev/null +++ b/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit } from '@angular/core'; + +import { BrowserRecommendation, BrowserSupportService } from '../../services/browser-support.service'; + +@Component({ + selector: 'os-unsupported-browser', + templateUrl: './unsupported-browser.component.html' +}) +export class UnsupportedBrowserComponent implements OnInit { + public supported: boolean; + public currentBrowser: string; + public browserVersion: string; + public supportedVersion: number; + + public get recommendedBrowsers(): BrowserRecommendation[] { + return this.browserSupprt.recommendedBrowsers; + } + + public constructor(private browserSupprt: BrowserSupportService) {} + + public ngOnInit(): void { + const deviceInfo = this.browserSupprt.getDeviceInfo(); + this.supported = this.browserSupprt.isBrowserSupported(); + this.currentBrowser = deviceInfo.browser; + this.browserVersion = deviceInfo.browser_version; + this.supportedVersion = this.browserSupprt.getSupportedVersion(deviceInfo); + } +} diff --git a/client/src/app/site/login/login.module.ts b/client/src/app/site/login/login.module.ts index 5b101d9ec..1975ec699 100644 --- a/client/src/app/site/login/login.module.ts +++ b/client/src/app/site/login/login.module.ts @@ -2,6 +2,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; +import { DeviceDetectorModule } from 'ngx-device-detector'; + import { LoginLegalNoticeComponent } from './components/login-legal-notice/login-legal-notice.component'; import { LoginMaskComponent } from './components/login-mask/login-mask.component'; import { LoginPrivacyPolicyComponent } from './components/login-privacy-policy/login-privacy-policy.component'; @@ -9,16 +11,18 @@ import { LoginWrapperComponent } from './components/login-wrapper/login-wrapper. import { ResetPasswordConfirmComponent } from './components/reset-password-confirm/reset-password-confirm.component'; import { ResetPasswordComponent } from './components/reset-password/reset-password.component'; import { SharedModule } from '../../shared/shared.module'; +import { UnsupportedBrowserComponent } from './components/unsupported-browser/unsupported-browser.component'; @NgModule({ - imports: [CommonModule, RouterModule, SharedModule], + imports: [CommonModule, RouterModule, SharedModule, DeviceDetectorModule], declarations: [ LoginWrapperComponent, ResetPasswordComponent, ResetPasswordConfirmComponent, LoginMaskComponent, LoginLegalNoticeComponent, - LoginPrivacyPolicyComponent + LoginPrivacyPolicyComponent, + UnsupportedBrowserComponent ] }) export class LoginModule {} diff --git a/client/src/app/site/login/services/browser-support.service.spec.ts b/client/src/app/site/login/services/browser-support.service.spec.ts new file mode 100644 index 000000000..3106528e1 --- /dev/null +++ b/client/src/app/site/login/services/browser-support.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { BrowserSupportService } from './browser-support.service'; + +describe('BrowserSupportService', () => { + let service: BrowserSupportService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BrowserSupportService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/login/services/browser-support.service.ts b/client/src/app/site/login/services/browser-support.service.ts new file mode 100644 index 000000000..b1a394738 --- /dev/null +++ b/client/src/app/site/login/services/browser-support.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core'; + +import { DeviceDetectorService, DeviceInfo } from 'ngx-device-detector'; + +const SmallestSupportedBrowserVersion = { + Chrome: 81, + Safari: 13, + Firefox: 68, + Opera: 66, + 'MS-Edge': 81, + 'MS-Edge-Chromium': 81 +}; + +const BrowserBlacklist = ['IE']; + +export interface BrowserRecommendation { + name: string; + url: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class BrowserSupportService { + public readonly recommendedBrowsers: BrowserRecommendation[] = [ + { + name: 'Google Chrome', + url: 'https://www.google.com/chrome/' + }, + { + name: 'Mozilla Firefox', + url: 'https://www.mozilla.org/firefox/' + } + ]; + + public constructor(private deviceService: DeviceDetectorService) {} + + public getDeviceInfo(): DeviceInfo { + return this.deviceService.getDeviceInfo(); + } + + public getSupportedVersion(info: DeviceInfo): number { + return SmallestSupportedBrowserVersion[info.browser]; + } + + /** + * Detect the browser version and forward to an error page if the browser was too old + */ + public isBrowserSupported(): boolean { + const deviceInfo = this.deviceService.getDeviceInfo(); + const browser = deviceInfo.browser; + const version = parseInt(deviceInfo.browser_version, 10); + + if (BrowserBlacklist.includes(browser)) { + return false; + } else if (!SmallestSupportedBrowserVersion[browser]) { + // if we don't know the browser, let's assume they are working + return true; + } else { + return version >= this.getSupportedVersion(deviceInfo); + } + } +}