// Copyright 2015 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import {ChromeEvent} from '/tools/typescript/definitions/chrome_event.js'; import {assert} from 'chrome://resources/js/assert_ts.js'; import {ActivityLogDelegate} from './activity_log/activity_log_history.js'; import {ActivityLogEventDelegate} from './activity_log/activity_log_stream.js'; import {ErrorPageDelegate} from './error_page.js'; import {ItemDelegate} from './item.js'; import {KeyboardShortcutDelegate} from './keyboard_shortcut_delegate.js'; import {LoadErrorDelegate} from './load_error.js'; import {Dialog, navigation, Page} from './navigation_helper.js'; import {PackDialogDelegate} from './pack_dialog.js'; import {SiteSettingsDelegate} from './site_settings_mixin.js'; import {ToolbarDelegate} from './toolbar.js'; export interface ServiceInterface extends ActivityLogDelegate, ActivityLogEventDelegate, ErrorPageDelegate, ItemDelegate, KeyboardShortcutDelegate, LoadErrorDelegate, PackDialogDelegate, SiteSettingsDelegate, ToolbarDelegate { notifyDragInstallInProgress(): void; loadUnpackedFromDrag(): Promise; installDroppedFile(): void; getProfileStateChangedTarget(): ChromeEvent<(info: chrome.developerPrivate.ProfileInfo) => void>; getProfileConfiguration(): Promise; getExtensionsInfo(): Promise; getExtensionSize(id: string): Promise; } export class Service implements ServiceInterface { private isDeleting_: boolean = false; private eventsToIgnoreOnce_: Set = new Set(); getProfileConfiguration() { return chrome.developerPrivate.getProfileConfiguration(); } getItemStateChangedTarget() { return chrome.developerPrivate.onItemStateChanged; } shouldIgnoreUpdate( extensionId: string, eventType: chrome.developerPrivate.EventType): boolean { return this.eventsToIgnoreOnce_.delete(`${extensionId}_${eventType}`); } ignoreNextEvent( extensionId: string, eventType: chrome.developerPrivate.EventType): void { this.eventsToIgnoreOnce_.add(`${extensionId}_${eventType}`); } getProfileStateChangedTarget() { return chrome.developerPrivate.onProfileStateChanged; } getExtensionsInfo() { return chrome.developerPrivate.getExtensionsInfo( {includeDisabled: true, includeTerminated: true}); } getExtensionSize(id: string) { return chrome.developerPrivate.getExtensionSize(id); } addRuntimeHostPermission(id: string, host: string): Promise { return chrome.developerPrivate.addHostPermission(id, host); } removeRuntimeHostPermission(id: string, host: string): Promise { return chrome.developerPrivate.removeHostPermission(id, host); } recordUserAction(metricName: string): void { chrome.metricsPrivate.recordUserAction(metricName); } /** * Opens a file browser dialog for the user to select a file (or directory). * @return The promise to be resolved with the selected path. */ private chooseFilePath_( selectType: chrome.developerPrivate.SelectType, fileType: chrome.developerPrivate.FileType): Promise { return chrome.developerPrivate.choosePath(selectType, fileType) .catch(error => { if (error.message !== 'File selection was canceled.') { throw error; } return ''; }); } updateExtensionCommandKeybinding( extensionId: string, commandName: string, keybinding: string) { chrome.developerPrivate.updateExtensionCommand({ extensionId: extensionId, commandName: commandName, keybinding: keybinding, }); } updateExtensionCommandScope( extensionId: string, commandName: string, scope: chrome.developerPrivate.CommandScope): void { // The COMMAND_REMOVED event needs to be ignored since it is sent before // the command is added back with the updated scope but can be handled // after the COMMAND_ADDED event. this.ignoreNextEvent( extensionId, chrome.developerPrivate.EventType.COMMAND_REMOVED); chrome.developerPrivate.updateExtensionCommand({ extensionId: extensionId, commandName: commandName, scope: scope, }); } setShortcutHandlingSuspended(isCapturing: boolean) { chrome.developerPrivate.setShortcutHandlingSuspended(isCapturing); } /** * @return A signal that loading finished, rejected if any error occurred. */ private loadUnpackedHelper_(extraOptions?: chrome.developerPrivate.LoadUnpackedOptions): Promise { const options = Object.assign( { failQuietly: true, populateError: true, }, extraOptions); return chrome.developerPrivate.loadUnpacked(options) .then(loadError => { if (loadError) { throw loadError; } // The load was successful if there's no loadError. return true; }) .catch(error => { if (error.message !== 'File selection was canceled.') { throw error; } return false; }); } deleteItem(id: string) { if (this.isDeleting_) { return; } chrome.metricsPrivate.recordUserAction('Extensions.RemoveExtensionClick'); this.isDeleting_ = true; chrome.management.uninstall(id, {showConfirmDialog: true}) .catch( _ => { // The error was almost certainly the user canceling the dialog. // Do nothing. We only check it so we don't get noisy logs. }) .finally(() => { this.isDeleting_ = false; }); } /** * Allows the consumer to call the API asynchronously. */ uninstallItem(id: string): Promise { chrome.metricsPrivate.recordUserAction('Extensions.RemoveExtensionClick'); return chrome.management.uninstall(id, {showConfirmDialog: true}); } deleteItems(ids: string[]): Promise { this.isDeleting_ = true; return chrome.developerPrivate.removeMultipleExtensions(ids).finally(() => { this.isDeleting_ = false; }); } setItemSafetyCheckWarningAcknowledged(id: string): Promise { return chrome.developerPrivate.updateExtensionConfiguration({ extensionId: id, acknowledgeSafetyCheckWarning: true, }); } setItemEnabled(id: string, isEnabled: boolean) { chrome.metricsPrivate.recordUserAction( isEnabled ? 'Extensions.ExtensionEnabled' : 'Extensions.ExtensionDisabled'); chrome.management.setEnabled(id, isEnabled); } setItemAllowedIncognito(id: string, isAllowedIncognito: boolean) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: id, incognitoAccess: isAllowedIncognito, }); } setItemAllowedOnFileUrls(id: string, isAllowedOnFileUrls: boolean) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: id, fileAccess: isAllowedOnFileUrls, }); } setItemHostAccess(id: string, hostAccess: chrome.developerPrivate.HostAccess): void { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: id, hostAccess: hostAccess, }); } setItemCollectsErrors(id: string, collectsErrors: boolean): void { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: id, errorCollection: collectsErrors, }); } setItemPinnedToToolbar(id: string, pinnedToToolbar: boolean) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: id, pinnedToToolbar, }); } inspectItemView(id: string, view: chrome.developerPrivate.ExtensionView): void { chrome.developerPrivate.openDevTools({ extensionId: id, renderProcessId: view.renderProcessId, renderViewId: view.renderViewId, incognito: view.incognito, isServiceWorker: view.type === 'EXTENSION_SERVICE_WORKER_BACKGROUND', }); } openUrl(url: string): void { window.open(url); } reloadItem(id: string): Promise { return chrome.developerPrivate .reload(id, {failQuietly: true, populateErrorForUnpacked: true}) .then(loadError => { if (loadError) { throw loadError; } }); } repairItem(id: string): void { chrome.developerPrivate.repairExtension(id); } showItemOptionsPage(extension: chrome.developerPrivate.ExtensionInfo): void { assert(extension && extension.optionsPage); if (extension.optionsPage!.openInTab) { chrome.developerPrivate.showOptions(extension.id); } else { navigation.navigateTo({ page: Page.DETAILS, subpage: Dialog.OPTIONS, extensionId: extension.id, }); } } setProfileInDevMode(inDevMode: boolean) { chrome.developerPrivate.updateProfileConfiguration( {inDeveloperMode: inDevMode}); } loadUnpacked(): Promise { return this.loadUnpackedHelper_(); } retryLoadUnpacked(retryGuid?: string): Promise { // Attempt to load an unpacked extension, optionally as another attempt at // a previously-specified load. return this.loadUnpackedHelper_({retryGuid}); } choosePackRootDirectory(): Promise { return this.chooseFilePath_( chrome.developerPrivate.SelectType.FOLDER, chrome.developerPrivate.FileType.LOAD); } choosePrivateKeyPath(): Promise { return this.chooseFilePath_( chrome.developerPrivate.SelectType.FILE, chrome.developerPrivate.FileType.PEM); } packExtension(rootPath: string, keyPath: string, flag?: number): Promise { return chrome.developerPrivate.packDirectory(rootPath, keyPath, flag); } updateAllExtensions(extensions: chrome.developerPrivate.ExtensionInfo[]) { /** * Attempt to reload local extensions. If an extension fails to load, the * user is prompted to try updating the broken extension using loadUnpacked * and we skip reloading the remaining local extensions. */ return chrome.developerPrivate.autoUpdate().then( () => { chrome.metricsPrivate.recordUserAction('Options_UpdateExtensions'); return new Promise((resolve, reject) => { const loadLocalExtensions = async () => { for (const extension of extensions) { if (extension.location === 'UNPACKED') { try { await this.reloadItem(extension.id); } catch (loadError) { reject(loadError); break; } } } resolve(); }; loadLocalExtensions(); }); }); } deleteErrors( extensionId: string, errorIds?: number[], type?: chrome.developerPrivate.ErrorType) { chrome.developerPrivate.deleteExtensionErrors({ extensionId: extensionId, errorIds: errorIds, type: type, }); } requestFileSource(args: chrome.developerPrivate.RequestFileSourceProperties): Promise { return chrome.developerPrivate.requestFileSource(args); } showInFolder(id: string) { chrome.developerPrivate.showPath(id); } getExtensionActivityLog(extensionId: string): Promise { return chrome.activityLogPrivate.getExtensionActivities( { activityType: chrome.activityLogPrivate.ExtensionActivityFilter.ANY, extensionId: extensionId, }, ); } getFilteredExtensionActivityLog(extensionId: string, searchTerm: string) { const anyType = chrome.activityLogPrivate.ExtensionActivityFilter.ANY; // Construct one filter for each API call we will make: one for substring // search by api call, one for substring search by page URL, and one for // substring search by argument URL. % acts as a wildcard. const activityLogFilters = [ { activityType: anyType, extensionId: extensionId, apiCall: `%${searchTerm}%`, }, { activityType: anyType, extensionId: extensionId, pageUrl: `%${searchTerm}%`, }, { activityType: anyType, extensionId: extensionId, argUrl: `%${searchTerm}%`, }, ]; const promises: Array> = activityLogFilters.map( filter => chrome.activityLogPrivate.getExtensionActivities(filter)); return Promise.all(promises).then(results => { // We may have results that are present in one or more searches, so // we merge them here. We also assume that every distinct activity // id corresponds to exactly one activity. const activitiesById = new Map(); for (const result of results) { for (const activity of result.activities) { activitiesById.set(activity.activityId, activity); } } return {activities: Array.from(activitiesById.values())}; }); } deleteActivitiesById(activityIds: string[]): Promise { return chrome.activityLogPrivate.deleteActivities(activityIds); } deleteActivitiesFromExtension(extensionId: string): Promise { return chrome.activityLogPrivate.deleteActivitiesByExtension(extensionId); } getOnExtensionActivity(): ChromeEvent< (activity: chrome.activityLogPrivate.ExtensionActivity) => void> { return chrome.activityLogPrivate.onExtensionActivity; } downloadActivities(rawActivityData: string, fileName: string) { const blob = new Blob([rawActivityData], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); } /** * Attempts to load an unpacked extension via a drag-n-drop gesture. * @return {!Promise} */ loadUnpackedFromDrag() { return this.loadUnpackedHelper_({useDraggedPath: true}); } installDroppedFile() { chrome.developerPrivate.installDroppedFile(); } notifyDragInstallInProgress() { chrome.developerPrivate.notifyDragInstallInProgress(); } getUserSiteSettings(): Promise { return chrome.developerPrivate.getUserSiteSettings(); } addUserSpecifiedSites( siteSet: chrome.developerPrivate.SiteSet, hosts: string[]): Promise { return chrome.developerPrivate.addUserSpecifiedSites({siteSet, hosts}); } removeUserSpecifiedSites( siteSet: chrome.developerPrivate.SiteSet, hosts: string[]): Promise { return chrome.developerPrivate.removeUserSpecifiedSites({siteSet, hosts}); } getUserAndExtensionSitesByEtld(): Promise { return chrome.developerPrivate.getUserAndExtensionSitesByEtld(); } getMatchingExtensionsForSite(site: string): Promise { return chrome.developerPrivate.getMatchingExtensionsForSite(site); } getUserSiteSettingsChangedTarget() { return chrome.developerPrivate.onUserSiteSettingsChanged; } setShowAccessRequestsInToolbar(id: string, showRequests: boolean) { chrome.developerPrivate.updateExtensionConfiguration({ extensionId: id, showAccessRequestsInToolbar: showRequests, }); } updateSiteAccess( site: string, updates: chrome.developerPrivate.ExtensionSiteAccessUpdate[]): Promise { return chrome.developerPrivate.updateSiteAccess(site, updates); } static getInstance(): ServiceInterface { return instance || (instance = new Service()); } static setInstance(obj: ServiceInterface) { instance = obj; } } let instance: ServiceInterface|null = null;