import angular from 'angular' ; import autobind from 'autobind-decorator' ; import browser , { Tabs } from 'webextension-polyfill' ; import { AlertService } from '../../../shared/alert/alert.service' ; import { BookmarkHelperService } from '../../../shared/bookmark/bookmark-helper/bookmark-helper.service' ; import { BaseError , FailedGetPageMetadataError , FailedRegisterAutoUpdatesError , I18nError } from '../../../shared/errors/errors' ; import * as Errors from '../../../shared/errors/errors' ; import Globals from '../../../shared/global-shared.constants' ; import { BrowserName , MessageCommand , PlatformType } from '../../../shared/global-shared.enum' ; import { I18nObject , PlatformService , WebpageMetadata } from '../../../shared/global-shared.interface' ; import { LogService } from '../../../shared/log/log.service' ; import { StoreService } from '../../../shared/store/store.service' ; import { SyncType } from '../../../shared/sync/sync.enum' ; import { Sync } from '../../../shared/sync/sync.interface' ; import { UtilityService } from '../../../shared/utility/utility.service' ; import { WorkingService } from '../../../shared/working/working.service' ; import { DownloadFileMessage , Message , SyncBookmarksMessage } from '../../webext.interface' ; import { WebExtBackgroundService } from '../../webext-background/webext-background.service' ; import { BookmarkIdMapperService } from '../bookmark-id-mapper/bookmark-id-mapper.service' ; @ autobind export abstract class WebExtPlatformService implements PlatformService { Strings = require ( '../../../../../res/strings/en.json' ) ; $injector : ng . auto . IInjectorService ; $interval : ng . IIntervalService ; $q : ng . IQService ; $timeout : ng . ITimeoutService ; alertSvc : AlertService ; _backgroundSvc : WebExtBackgroundService | undefined ; bookmarkIdMapperSvc : BookmarkIdMapperService ; bookmarkHelperSvc : BookmarkHelperService ; logSvc : LogService ; storeSvc : StoreService ; utilitySvc : UtilityService ; workingSvc : WorkingService ; contentScriptUrl = 'assets/webpage-metadata-collecter.js' ; optionalPermissions = { origins : [ 'http://*/' , 'https://*/' ] } ; refreshInterfaceTimeout : any ; static $inject = [ '$injector' , '$interval' , '$q' , '$timeout' , 'AlertService' , 'BookmarkHelperService' , 'BookmarkIdMapperService' , 'LogService' , 'StoreService' , 'UtilityService' , 'WorkingService' ] ; constructor ( $injector , $interval , $q , $timeout , AlertSvc , BookmarkHelperSvc , BookmarkIdMapperSvc , LogSvc , StoreSvc , UtilitySvc , WorkingSvc ) { this . $injector = $injector ; this . $interval = $interval ; this . $q = $q ; this . $timeout = $timeout ; this . alertSvc = AlertSvc ; this . bookmarkIdMapperSvc = BookmarkIdMapperSvc ; this . bookmarkHelperSvc = BookmarkHelperSvc ; this . logSvc = LogSvc ; this . storeSvc = StoreSvc ; this . utilitySvc = UtilitySvc ; this . workingSvc = WorkingSvc ; } platformName = '' ; get backgroundSvc ( ) : WebExtBackgroundService { if ( angular . isUndefined ( this . _backgroundSvc ) ) { this . _backgroundSvc = this . $injector . get ( 'WebExtBackgroundService' ) ; } return this . _backgroundSvc as WebExtBackgroundService ; } checkOptionalNativePermissions ( ) { return this . $q . resolve ( ) . then ( ( ) => { return browser . permissions . contains ( this . optionalPermissions ) ; } ) ; } disableNativeEventListeners ( ) { return this . sendMessage ( { command : MessageCommand . DisableEventListeners } ) ; } disableSync ( ) { return this . sendMessage ( { command : MessageCommand . DisableSync } ) ; } downloadFile ( filename , textContents , displaySaveDialog = true ) { const message = { command : MessageCommand . DownloadFile , displaySaveDialog , filename , textContents } ; return this . sendMessage ( message ) ; } enableNativeEventListeners ( ) { return this . sendMessage ( { command : MessageCommand . EnableEventListeners } ) ; } getAppVersion ( ) { const appVersion = browser . runtime . getManifest ( ) . version ; return this . $q . resolve ( this . utilitySvc . getSemVerAlignedVersion ( appVersion ) ) ; } getAppVersionName ( ) { return this . $q . resolve ( ( browser . runtime . getManifest ( ) as any ) . version_name ) ; } getCurrentLocale ( ) { return this . $q . resolve ( browser . i18n . getUILanguage ( ) ) ; } getCurrentUrl ( ) { return browser . tabs . query ( { currentWindow : true , active : true } ) . then ( ( tabs ) => { const [ activeTab ] = tabs ; return activeTab . url ?? '' ; } ) ; } getI18nString ( i18nObj ) { let i18nStr ; let platformName = this . platformName . toString ( ) ; if ( platformName === PlatformType . Chromium ) { const browserName = this . utilitySvc . getBrowserName ( ) as string ; platformName = browserName !== BrowserName . Chrome ? browserName : platformName ; } if ( Object . keys ( i18nObj ) . includes ( platformName ) ) { i18nStr = browser . i18n . getMessage ( `${ i18nObj . key }_${ platformName }` ) ; } else { i18nStr = browser . i18n . getMessage ( `${ i18nObj . key }_Default` ) ; } if ( angular . isUndefined ( i18nStr ?? undefined ) ) { throw new I18nError ( 'I18n string has no value' ) ; } return i18nStr ; } abstract getNewTabUrl ( ) ; getPageMetadata ( getFullMetadata = true , pageUrl ? ) { return browser . tabs . query ( { active : true , currentWindow : true } ) . then ( ( tabs ) => { const [ activeTab ] = tabs ; if ( ! activeTab ) { throw new FailedGetPageMetadataError ( ) ; } let metadata = activeTab && { title : activeTab . title , url : activeTab . url } ; if ( getFullMetadata ) { getFullMetadata = ! this . urlIsNativeConfigPage ( activeTab . url ) ; } if ( ! getFullMetadata ) { return metadata ; } return browser . tabs . executeScript ( activeTab . id , { file : this . contentScriptUrl } ) . then ( ( ) => { return browser . tabs . executeScript ( activeTab . id , { code : 'WebpageMetadataCollecter.CollectMetadata();' } ) ; } ) . then ( ( response ) => { if ( response ?. length && response ?. [ 0 ] ) { [ metadata ] = response ; } metadata . title = metadata . title ?? activeTab . title ; metadata . url = metadata . url ?? activeTab . url ; return metadata ; } ) . catch ( ( err ) => { this . logSvc . logWarning ( `Unable to get metadata: ${ err ? err . message : '' }` ) ; return metadata ; } ) ; } ) ; } openUrl ( url ) { const createProperties = { } ; const openInNewTab = ( urlToOpen ? ) => { if ( urlToOpen ) { createProperties . url = urlToOpen ; } return browser . tabs . create ( createProperties ) . then ( window . close ) ; } ; if ( ! this . urlIsSupported ( url ) ) { this . logSvc . logInfo ( `Attempted to navigate to unsupported url: ${ url }` ) ; openInNewTab ( ) ; return ; } browser . tabs . query ( { currentWindow : true , active : true } ) . then ( ( tabs ) => { const [ activeTab ] = tabs ; return tabs . length > 0 && activeTab . url && activeTab . url . startsWith ( this . getNewTabUrl ( ) ) ? browser . tabs . update ( activeTab . id , { url } ) . then ( window . close ) : openInNewTab ( url ) ; } ) . catch ( openInNewTab ) ; } queueLocalResync ( ) { return this . queueSync ( { type : SyncType . Local } ) . then ( ( ) => { this . logSvc . logInfo ( 'Local sync data refreshed' ) ; } ) ; } queueSync ( sync ? , command = MessageCommand . SyncBookmarks , runSync = true ) { const message = { command , sync , runSync } ; return this . sendMessage ( message ) . finally ( this . workingSvc . hide ) ; } refreshNativeInterface ( syncEnabled ? , syncType ? ) { let iconPath ; let newTitle = this . getI18nString ( this . Strings . App . Title ) ; const syncingTitle = ` (${ this . getI18nString ( this . Strings . Tooltip . Syncing ) })` ; const syncedTitle = ` (${ this . getI18nString ( this . Strings . Tooltip . Synced ) })` ; const notSyncedTitle = ` (${ this . getI18nString ( this . Strings . Tooltip . NotSynced ) })` ; if ( this . refreshInterfaceTimeout ) { this . $timeout . cancel ( this . refreshInterfaceTimeout ) ; this . refreshInterfaceTimeout = null ; } if ( syncType ) { iconPath = syncType === SyncType . Local ? `${ Globals . PathToAssets }/downloading.png` : `${ Globals . PathToAssets }/uploading.png` ; newTitle += syncingTitle ; } else if ( syncEnabled ) { iconPath = `${ Globals . PathToAssets }/synced.png` ; newTitle += syncedTitle ; } else { iconPath = `${ Globals . PathToAssets }/notsynced.png` ; newTitle += notSyncedTitle ; } return this . $q ( ( resolve , reject ) => { const iconUpdated = this . $q . defer < void > ( ) ; const titleUpdated = this . $q . defer < void > ( ) ; browser . browserAction . getTitle ( { } ) . then ( ( currentTitle ) => { if ( newTitle === currentTitle ) { return resolve ( ) ; } if ( currentTitle . indexOf ( syncingTitle ) > 0 && newTitle . indexOf ( syncedTitle ) ) { this . refreshInterfaceTimeout = this . $timeout ( ( ) => { browser . browserAction . setIcon ( { path : iconPath } ) ; browser . browserAction . setTitle ( { title : newTitle } ) ; } , 350 ) ; iconUpdated . resolve ( ) ; titleUpdated . resolve ( ) ; } else { browser . browserAction . setIcon ( { path : iconPath } ) . then ( iconUpdated . resolve ) ; browser . browserAction . setTitle ( { title : newTitle } ) . then ( titleUpdated . resolve ) ; } this . $q . all ( [ iconUpdated , titleUpdated ] ) . then ( resolve ) . catch ( reject ) ; } ) ; } ) ; } sendMessage ( message ) { let module ; try { module = angular . module ( 'WebExtBackgroundModule' ) ; } catch ( err ) { } let promise ; if ( angular . isUndefined ( module ) ) { promise = browser . runtime . sendMessage ( message ) ; } else { promise = this . backgroundSvc . onMessage ( message ) ; } return promise . catch ( ( err ) => { const error = new ( < any > Errors ) [ err . message ] ( ) ; error . logged = true ; throw error ; } ) ; } startSyncUpdateChecks ( ) { return browser . alarms . clear ( Globals . Alarms . SyncUpdatesCheck . Name ) . then ( ( ) => { return browser . alarms . create ( Globals . Alarms . SyncUpdatesCheck . Name , { periodInMinutes : Globals . Alarms . SyncUpdatesCheck . Period } ) ; } ) . catch ( ( err ) => { throw new FailedRegisterAutoUpdatesError ( undefined , err ) ; } ) ; } stopSyncUpdateChecks ( ) { return browser . alarms . clear ( Globals . Alarms . SyncUpdatesCheck . Name ) . then ( ( ) => { } ) ; } urlIsNativeConfigPage ( url ? ) { return false ; } urlIsSupported ( url ) { return true ; } }