// 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 new Promise(function(resolve) { chrome.developerPrivate.getProfileConfiguration(resolve); }); } 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 new Promise(function( resolve) { chrome.developerPrivate.getExtensionsInfo( {includeDisabled: true, includeTerminated: true}, resolve); }); } getExtensionSize(id: string) { return new Promise(function(resolve) { chrome.developerPrivate.getExtensionSize(id, resolve); }); } addRuntimeHostPermission(id: string, host: string): Promise { return new Promise((resolve, reject) => { chrome.developerPrivate.addHostPermission(id, host, () => { if (chrome.runtime.lastError) { reject(chrome.runtime.lastError.message); return; } resolve(); }); }); } removeRuntimeHostPermission(id: string, host: string): Promise { return new Promise((resolve, reject) => { chrome.developerPrivate.removeHostPermission(id, host, () => { if (chrome.runtime.lastError) { reject(chrome.runtime.lastError.message); return; } resolve(); }); }); } 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 new Promise(function(resolve, reject) { chrome.developerPrivate.choosePath(selectType, fileType, function(path) { if (chrome.runtime.lastError && chrome.runtime.lastError.message !== 'File selection was canceled.') { reject(chrome.runtime.lastError); } else { resolve(path || ''); } }); }); } 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 { return new Promise(function(resolve, reject) { const options = Object.assign( { failQuietly: true, populateError: true, }, extraOptions); chrome.developerPrivate.loadUnpacked(options, (loadError) => { if (chrome.runtime.lastError && chrome.runtime.lastError.message !== 'File selection was canceled.') { throw new Error(chrome.runtime.lastError.message); } if (loadError) { return reject(loadError); } // The load was successful if there's no lastError indicated (and // no loadError, which is checked above). const loadSuccessful = typeof chrome.runtime.lastError === 'undefined'; resolve(loadSuccessful); }); }); } deleteItem(id: string) { if (this.isDeleting_) { return; } chrome.metricsPrivate.recordUserAction('Extensions.RemoveExtensionClick'); this.isDeleting_ = true; chrome.management.uninstall(id, {showConfirmDialog: true}, () => { // The "last error" was almost certainly the user canceling the dialog. // Do nothing. We only check it so we don't get noisy logs. /** @suppress {suspiciousCode} */ chrome.runtime.lastError; this.isDeleting_ = false; }); } 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, }); } 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 new Promise(function(resolve, reject) { chrome.developerPrivate.reload( id, {failQuietly: true, populateErrorForUnpacked: true}, (loadError) => { if (loadError) { reject(loadError); return; } resolve(); }); }); } 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: 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, callback?: (response: chrome.developerPrivate.PackDirectoryResponse) => void): void { chrome.developerPrivate.packDirectory(rootPath, keyPath, flag, callback); } 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 new Promise((resolve) => { chrome.developerPrivate.autoUpdate(() => resolve()); chrome.metricsPrivate.recordUserAction('Options_UpdateExtensions'); }) .then(() => { 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 new Promise(function(resolve) { chrome.developerPrivate.requestFileSource(args, resolve); }); } showInFolder(id: string) { chrome.developerPrivate.showPath(id); } getExtensionActivityLog(extensionId: string): Promise { return new Promise(function(resolve) { chrome.activityLogPrivate.getExtensionActivities( { activityType: chrome.activityLogPrivate.ExtensionActivityFilter.ANY, extensionId: extensionId, }, resolve); }); } 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 => new Promise(function(resolve) { chrome.activityLogPrivate.getExtensionActivities( filter, resolve); })); 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 new Promise(function(resolve) { chrome.activityLogPrivate.deleteActivities(activityIds, resolve); }); } deleteActivitiesFromExtension(extensionId: string): Promise { return new Promise(function(resolve) { chrome.activityLogPrivate.deleteActivitiesByExtension( extensionId, resolve); }); } 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 new Promise(function(resolve) { chrome.developerPrivate.getUserSiteSettings(resolve); }); } addUserSpecifiedSites( siteSet: chrome.developerPrivate.SiteSet, hosts: string[]): Promise { return new Promise(function(resolve) { chrome.developerPrivate.addUserSpecifiedSites({siteSet, hosts}, resolve); }); } removeUserSpecifiedSites( siteSet: chrome.developerPrivate.SiteSet, hosts: string[]): Promise { return new Promise(function(resolve) { chrome.developerPrivate.removeUserSpecifiedSites( {siteSet, hosts}, resolve); }); } getUserAndExtensionSitesByEtld(): Promise { return new Promise(function(resolve) { chrome.developerPrivate.getUserAndExtensionSitesByEtld(resolve); }); } 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, }); } static getInstance(): ServiceInterface { return instance || (instance = new Service()); } static setInstance(obj: ServiceInterface) { instance = obj; } } let instance: ServiceInterface|null = null;