mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-01 00:38:50 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			623 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			623 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | |
| /* vim: set sts=2 sw=2 et tw=80: */
 | |
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| 
 | |
| const lazy = XPCOMUtils.declareLazy({
 | |
|   AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
 | |
|   DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
 | |
|   FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
 | |
|   KeyValueService: "resource://gre/modules/kvstore.sys.mjs",
 | |
|   MENU_STORE_WRITE_DEBOUNCE_TIME: {
 | |
|     pref: "extensions.webextensions.menus.writeDebounceTime",
 | |
|     // Minimum 0ms, max 1min
 | |
|     transform: value => Math.min(Math.max(value, 0), 1 * 60 * 1000),
 | |
|     default: 5000, // Default to 5s
 | |
|   },
 | |
| });
 | |
| 
 | |
| const SCHEMA_VERSION = 1;
 | |
| const KVSTORE_DIRNAME = "extension-store-menus";
 | |
| 
 | |
| /**
 | |
|  * MenuId represent the type of the ids associated to the extension
 | |
|  * created menus, which is expected to be of type:
 | |
|  *
 | |
|  * - string
 | |
|  * - or an auto-generated integer (for menus created without a pre-assigned menu id,
 | |
|  *   only allowed for extensions with a persistent background script or without any background
 | |
|  *   context at all).
 | |
|  *
 | |
|  * Only menus registered by extensions with a non-persistent backgrond context are going
 | |
|  * to be persisted across sessions, and their id is always a string.
 | |
|  *
 | |
|  * @typedef {number} integer
 | |
|  * @typedef {string|integer} MenuId
 | |
|  *
 | |
| 
 | |
| /**
 | |
|  * This class manages the extensions menus stored on disk across
 | |
|  * all extensions (with kvstore as the underlying backend).
 | |
|  */
 | |
| class ExtensionMenusStore {
 | |
|   #store = null;
 | |
|   #initPromise = null;
 | |
| 
 | |
|   /**
 | |
|    * Determine if the menus created by the given extension should
 | |
|    * be persisted on disk.
 | |
|    *
 | |
|    * @param {Extension} extension
 | |
|    *
 | |
|    * @returns {boolean} Returns true if the menus should be persisted on disk.
 | |
|    */
 | |
|   static shouldPersistMenus(extension) {
 | |
|     return extension.manifest.background && !extension.persistentBackground;
 | |
|   }
 | |
| 
 | |
|   get storePath() {
 | |
|     const { path: storePath } = lazy.FileUtils.getDir("ProfD", [
 | |
|       KVSTORE_DIRNAME,
 | |
|     ]);
 | |
|     return storePath;
 | |
|   }
 | |
| 
 | |
|   async #asyncInit() {
 | |
|     const { storePath } = this;
 | |
|     await IOUtils.makeDirectory(storePath, { ignoreExisting: true });
 | |
|     this.#store = await lazy.KeyValueService.getOrCreateWithOptions(
 | |
|       storePath,
 | |
|       "menus",
 | |
|       { strategy: lazy.KeyValueService.RecoveryStrategy.RENAME }
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   #lazyInit() {
 | |
|     if (!this.#initPromise) {
 | |
|       this.#initPromise = this.#asyncInit();
 | |
|     }
 | |
| 
 | |
|     return this.#initPromise;
 | |
|   }
 | |
| 
 | |
|   #notifyPersistedMenusUpdatedForTesting(extensionId) {
 | |
|     Services.obs.notifyObservers(
 | |
|       null,
 | |
|       "webext-persisted-menus-updated",
 | |
|       extensionId
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * An helper method to check if the store includes data for the given extension (ID).
 | |
|    *
 | |
|    * @param {string} extensionId An extension ID
 | |
|    * @returns {Promise<boolean>} true if the store includes data for the given
 | |
|    * extension, false otherwise.
 | |
|    */
 | |
|   async hasExtensionData(extensionId) {
 | |
|     await this.#lazyInit();
 | |
|     return this.#store.has(extensionId);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns all the persisted menus for a given extension (ID), or an empty map
 | |
|    * if there isn't any data for the given extension id and extension version.
 | |
|    *
 | |
|    * @param {string} extensionId An extension ID
 | |
|    * @param {string} currentExtensionVersion The current extension version
 | |
|    * @returns {Promise<Map>} A map of persisted menu details.
 | |
|    */
 | |
|   async getPersistedMenus(extensionId, currentExtensionVersion) {
 | |
|     await this.#lazyInit();
 | |
| 
 | |
|     let value;
 | |
|     try {
 | |
|       value = await this.#store.get(extensionId);
 | |
|     } catch (err) {
 | |
|       Cu.reportError(
 | |
|         `Error on retrieving stored menus for ${extensionId}: ${err}\n`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (!value) {
 | |
|       return new Map();
 | |
|     }
 | |
| 
 | |
|     const { menuSchemaVersion, extensionVersion, menus } = JSON.parse(value);
 | |
| 
 | |
|     // Drop the stored data if the extension version is not matching the
 | |
|     // current extension version.
 | |
|     if (extensionVersion != currentExtensionVersion) {
 | |
|       return new Map();
 | |
|     }
 | |
| 
 | |
|     // NOTE: future version may use the following block to convert stored menus
 | |
|     // data.
 | |
|     if (menuSchemaVersion !== SCHEMA_VERSION) {
 | |
|       Cu.reportError(
 | |
|         `Dropping stored menus for ${extensionId} due to unxpected menuSchemaVersion ${menuSchemaVersion} (expected ${SCHEMA_VERSION})`
 | |
|       );
 | |
|       // TODO: should we consider firing onInstalled if we had to drop stored
 | |
|       // menus due to a schema mismatch? should we do the same in case of
 | |
|       // corrupted storage?
 | |
|       return new Map();
 | |
|     }
 | |
| 
 | |
|     return new Map(menus);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Updates the map of persisted menus for a given extension (ID).
 | |
|    *
 | |
|    * We store each menu registered by extensions as an array of
 | |
|    * key/value pairs (derived from the Map of MenuCreateProperties).
 | |
|    *
 | |
|    * The format on disk should look like this one:
 | |
|    *
 | |
|    * ```
 | |
|    * {
 | |
|    *   "@extension-id1": { menuSchemaVersion: N, menus: [["menuid-1", <MenuCreateProperties>], ...] },
 | |
|    *   "@extension-id2": { menuSchemaVersion: N, menus: [["menuid-2", <MenuCreateProperties>], ...] },
 | |
|    * }
 | |
|    * ```
 | |
|    *
 | |
|    * @param {string} extensionId An extension ID
 | |
|    * @returns {Promise<void>}
 | |
|    */
 | |
|   async updatePersistedMenus(extensionId, extensionVersion, menusMap) {
 | |
|     await this.#lazyInit();
 | |
|     await this.#store.put(
 | |
|       extensionId,
 | |
|       JSON.stringify({
 | |
|         menuSchemaVersion: SCHEMA_VERSION,
 | |
|         extensionVersion: extensionVersion,
 | |
|         menus: Array.from(menusMap.entries()),
 | |
|       })
 | |
|     );
 | |
|     this.#notifyPersistedMenusUpdatedForTesting(extensionId);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Clears all the menus persisted on disk for a given extension (ID)
 | |
|    * being uninstalled.
 | |
|    *
 | |
|    * @param {string} extensionId An extension ID
 | |
|    */
 | |
|   async clearPersistedMenusOnUninstall(extensionId) {
 | |
|     const { storePath } = this;
 | |
|     const kvstoreDirExists = await IOUtils.exists(storePath);
 | |
|     if (!kvstoreDirExists) {
 | |
|       // Avoid to create an unnecessary kvstore directory (through the call
 | |
|       // to lazyInit). If one doesn't already, then there isn't any data
 | |
|       // to clear.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     await this.#lazyInit();
 | |
|     await this.#store.delete(extensionId);
 | |
|     this.#notifyPersistedMenusUpdatedForTesting(extensionId);
 | |
|   }
 | |
| }
 | |
| 
 | |
| let store = new ExtensionMenusStore();
 | |
| 
 | |
| /**
 | |
|  * This class manages the extensions menus for a specific extension.
 | |
|  *
 | |
|  * For extensions with a persistent background extension context
 | |
|  * or without any background extension the menus are kept only in memory,
 | |
|  * whereas for extensions with a non persistent background context
 | |
|  * the menus are also persisted on disk.
 | |
|  */
 | |
| class ExtensionMenusManager {
 | |
|   #writeToStoreTask = null;
 | |
|   #shutdownBlockerFn = null;
 | |
| 
 | |
|   constructor(extension) {
 | |
|     if (extension.hasShutdown) {
 | |
|       throw new Error(
 | |
|         `Error on creating new ExtensionMenusManager after extension shutdown: ${extension.id}`
 | |
|       );
 | |
|     }
 | |
|     this.extensionId = extension.id;
 | |
|     this.extensionVersion = extension.version;
 | |
|     this.persistMenusData = ExtensionMenusStore.shouldPersistMenus(extension);
 | |
|     // Map[MenuId -> MenuCreateProperties]
 | |
|     this.menus = null;
 | |
|     if (this.persistMenusData) {
 | |
|       extension.callOnClose(this);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async _finalizeStoreTaskForTesting() {
 | |
|     if (this.#writeToStoreTask && !this.#writeToStoreTask.isFinalized) {
 | |
|       await this.#writeToStoreTask;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   close() {
 | |
|     if (!this.#shutdownBlockerFn) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const shutdownBlockerFn = this.#shutdownBlockerFn;
 | |
|     shutdownBlockerFn().then(() => {
 | |
|       lazy.AsyncShutdown.profileBeforeChange.removeBlocker(shutdownBlockerFn);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   async asyncInit() {
 | |
|     if (this.menus) {
 | |
|       // ExtensionMenusManager is expected to be called only once from
 | |
|       // ExtensionMenus.asyncInitForExtension, which is expected to be
 | |
|       // called only once per extension (from the ext-menus onStartup
 | |
|       // lifecycle method).
 | |
|       Cu.reportError(
 | |
|         `ExtensionMenusManager for ${this.extensionId} should not be initialized more than once`
 | |
|       );
 | |
| 
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (!this.persistMenusData) {
 | |
|       this.menus = new Map();
 | |
|     }
 | |
| 
 | |
|     this.menus = await store
 | |
|       .getPersistedMenus(this.extensionId, this.extensionVersion)
 | |
|       .catch(err => {
 | |
|         Cu.reportError(
 | |
|           `Error loading ${this.extensionId} persisted menus: ${err}`
 | |
|         );
 | |
|         return new Map();
 | |
|       });
 | |
|   }
 | |
| 
 | |
|   #ensureInitialized() {
 | |
|     // ExtensionMenusStore instance for each extension using the menus API
 | |
|     // is expected to be done from the menus API onStartup lifecycle method.
 | |
|     if (!this.menus) {
 | |
|       throw new Error(
 | |
|         `ExtensionMenusStore instance for ${this.extensionId} is not initialized`
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Synchronously retrieve the map of the extension menus.
 | |
|    *
 | |
|    * For extensions that should persist menus across sessions the map is
 | |
|    * initialized from the data stored on disk and so this method is expected
 | |
|    * to only be called after ext-menus onStartup lifecycle method have
 | |
|    * called asyncInit on the instance of this class.
 | |
|    *
 | |
|    * @returns {Map<MenuId, object>} Map of the menus createProperties keyed by
 | |
|    * menu id.
 | |
|    */
 | |
|   getMenus() {
 | |
|     this.#ensureInitialized();
 | |
|     return this.menus;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Add or update menu data and optionally reparent menus (used when the update to
 | |
|    * a menu includes a different parentId). A DeferredTask scheduled by this
 | |
|    * method will update all menus data stored on disk for extensions that should
 | |
|    * persist menus across sessions.
 | |
|    *
 | |
|    * @param {object}  menuDetails The createProperties for the menu
 | |
|    * to add or update.
 | |
|    * @param {boolean} [reparent=false] Set to true if the menu should also
 | |
|    * be reparented.
 | |
|    */
 | |
|   updateMenus(menuDetails, reparent = false) {
 | |
|     this.#ensureInitialized();
 | |
| 
 | |
|     if (this.persistMenusData && reparent) {
 | |
|       // Make sure the reparent menu item is appended at the end (and so for sure
 | |
|       // after the menu item that will become its new parent).
 | |
|       // This is necessary if menuDetails.parentId is set (because it may point
 | |
|       // to a menu entry that appears after the current entry in the Map), but we
 | |
|       // still do it unconditionally (even if parentId is null) to make sure the
 | |
|       // relocated menu item is always rendered at the bottom.
 | |
|       this.menus.delete(menuDetails.id);
 | |
|     }
 | |
|     this.menus.set(menuDetails.id, menuDetails);
 | |
| 
 | |
|     if (!this.persistMenusData) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (reparent) {
 | |
|       // The menu items are ordered, with child menu items always following its parent.
 | |
|       // The logic below moves menu item registrations as needed to ensure a consistent order.
 | |
|       let menuIds = new Set();
 | |
|       menuIds.add(menuDetails.id);
 | |
|       // Iterate over a copy of the entries because we may modify the menus Map.
 | |
|       for (let [id, menuCreateDetails] of Array.from(this.menus)) {
 | |
|         if (menuIds.has(menuCreateDetails.parentId)) {
 | |
|           // Remember menu ID to detect its children.
 | |
|           menuIds.add(id);
 | |
|           // Append menu items to the end, to ensure that child menu items always follow the parent.
 | |
|           this.menus.delete(id);
 | |
|           this.menus.set(id, menuCreateDetails);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.#scheduleWriteToStoreTask();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Delete the given menu ids. A DeferredTask will update all menus
 | |
|    * data stored on disk for extensions that should persist menus across sessions.
 | |
|    *
 | |
|    * @param {Array<MenuId>}  menuIds Array of menu ids to remove.
 | |
|    */
 | |
|   deleteMenus(menuIds) {
 | |
|     this.#ensureInitialized();
 | |
|     for (const menuId of menuIds) {
 | |
|       this.menus.delete(menuId);
 | |
|     }
 | |
|     if (!this.persistMenusData) {
 | |
|       return;
 | |
|     }
 | |
|     this.#scheduleWriteToStoreTask();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Delete all menus. A DeferredTask scheduled by this method will update all menus
 | |
|    * data stored on disk for extensions that should persist menus across sessions.
 | |
|    */
 | |
|   deleteAllMenus() {
 | |
|     this.#ensureInitialized();
 | |
|     let alreadyEmpty = !this.menus.size;
 | |
|     this.menus.clear();
 | |
|     if (!this.persistMenusData || alreadyEmpty) {
 | |
|       return;
 | |
|     }
 | |
|     this.#scheduleWriteToStoreTask();
 | |
|   }
 | |
| 
 | |
|   #scheduleWriteToStoreTask() {
 | |
|     this.#ensureInitialized();
 | |
|     if (!this.#writeToStoreTask) {
 | |
|       this.#writeToStoreTask = new lazy.DeferredTask(
 | |
|         () => this.#writeToStoreNow(),
 | |
|         lazy.MENU_STORE_WRITE_DEBOUNCE_TIME
 | |
|       );
 | |
|       this.#shutdownBlockerFn = async () => {
 | |
|         if (!this.#writeToStoreTask || this.#writeToStoreTask.isFinalized) {
 | |
|           return;
 | |
|         }
 | |
|         await this.#writeToStoreTask.finalize();
 | |
|         this.#writeToStoreTask = null;
 | |
|         this.#shutdownBlockerFn = null;
 | |
|       };
 | |
|       lazy.AsyncShutdown.profileBeforeChange.addBlocker(
 | |
|         `Flush "${this.extensionId}" persisted menus to disk`,
 | |
|         this.#shutdownBlockerFn
 | |
|       );
 | |
|     }
 | |
|     this.#writeToStoreTask.arm();
 | |
|   }
 | |
| 
 | |
|   async #writeToStoreNow() {
 | |
|     this.#ensureInitialized();
 | |
|     await store.updatePersistedMenus(
 | |
|       this.extensionId,
 | |
|       this.extensionVersion,
 | |
|       this.menus
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Singleton providing a collection of methods used by
 | |
|  * ext-menus.js (and tests) to interact with the underlying classes.
 | |
|  */
 | |
| export const ExtensionMenus = {
 | |
|   KVSTORE_DIRNAME,
 | |
| 
 | |
|   // WeakMap<Extension, { promise: Promise<ExtensionMenusManager>, instance: ExtensionMenusManager}>
 | |
|   _menusManagers: new WeakMap(),
 | |
| 
 | |
|   /**
 | |
|    * Determine if the menus created by the given extension should
 | |
|    * be persisted on disk.
 | |
|    *
 | |
|    * @param {Extension} extension
 | |
|    *
 | |
|    * @returns {boolean} Returns true if the menus should be persisted on disk.
 | |
|    */
 | |
|   shouldPersistMenus(extension) {
 | |
|     return ExtensionMenusStore.shouldPersistMenus(extension);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Create and initialize ExtensionMenusManager instance
 | |
|    * for the given extension.  Used by ext-menus.js onStartup
 | |
|    * lifecycle method.
 | |
|    *
 | |
|    * @param {Extension} extension
 | |
|    *
 | |
|    * @returns {Promise<void>} A promise resolved when the
 | |
|    * ExtensionMenusManager instance is fully initialized
 | |
|    * (and persisted menus data loaded from disk for the
 | |
|    * extensions with a non-persistent background script).
 | |
|    */
 | |
|   async asyncInitForExtension(extension) {
 | |
|     let { promise } = this._menusManagers.get(extension) ?? {};
 | |
|     if (promise) {
 | |
|       return promise;
 | |
|     }
 | |
| 
 | |
|     const instance = new ExtensionMenusManager(extension);
 | |
|     extension.callOnClose({
 | |
|       close: () => {
 | |
|         this._menusManagers.delete(extension);
 | |
|       },
 | |
|     });
 | |
|     promise = instance.asyncInit().then(() => instance);
 | |
|     this._menusManagers.set(extension, { promise, instance });
 | |
|     return promise;
 | |
|   },
 | |
| 
 | |
|   _getManager(extension) {
 | |
|     const { instance } = this._menusManagers.get(extension) ?? {};
 | |
|     if (!instance) {
 | |
|       throw new Error(
 | |
|         `No ExtensionMenusManager instance found for ${extension.id}`
 | |
|       );
 | |
|     }
 | |
|     return instance;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Helper function used to normalize and merge menus
 | |
|    * create and update properties.
 | |
|    *
 | |
|    * @param {object} obj The target object
 | |
|    * @param {object} properties The properties to merge
 | |
|    * on the target object.
 | |
|    *
 | |
|    * @returns {object} The target object updated with
 | |
|    * the merged properties.
 | |
|    */
 | |
|   mergeMenuProperties(obj, properties) {
 | |
|     // The menu properties are being normalized based on
 | |
|     // the API JSONSchema definitions, and so we can
 | |
|     // rely on expecting properties not specified to be
 | |
|     // set to null, besides "icons" which is expected to
 | |
|     // be omitted when not explicitly specified (due to
 | |
|     // the use of `"optional": "omit-key-if-missing"` in
 | |
|     // its schema definition).
 | |
|     for (let propName in properties) {
 | |
|       if (properties[propName] === null) {
 | |
|         // Omitted optional argument.
 | |
|         continue;
 | |
|       }
 | |
|       obj[propName] = properties[propName];
 | |
|     }
 | |
| 
 | |
|     if ("icons" in properties && properties.icons === null && obj.icons) {
 | |
|       obj.icons = null;
 | |
|     }
 | |
| 
 | |
|     return obj;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Synchronously retrieve the map of the extension menus.
 | |
|    * Expected to only be called after ext-menus onStartup lifecycle
 | |
|    * method has already initialized the ExtensionMenusManager through
 | |
|    * a call to ExtensionMenus.asyncInitForExtension.
 | |
|    *
 | |
|    * @returns {Map<MenuId, object>} Map of the menus createProperties keyed by
 | |
|    * menu id.
 | |
|    */
 | |
|   getMenus(extension) {
 | |
|     return this._getManager(extension).getMenus();
 | |
|   },
 | |
| 
 | |
|   _getStoredMenusForTesting(extensionId, extensionVersion) {
 | |
|     return store.getPersistedMenus(extensionId, extensionVersion);
 | |
|   },
 | |
| 
 | |
|   _hasStoredExtensionData(extensionId) {
 | |
|     return store.hasExtensionData(extensionId);
 | |
|   },
 | |
| 
 | |
|   _getStoreForTesting() {
 | |
|     return store;
 | |
|   },
 | |
| 
 | |
|   _recreateStoreForTesting() {
 | |
|     store = new ExtensionMenusStore();
 | |
|     return store;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Add a new extension menu for the given extension. A DeferredTask
 | |
|    * will update all menus data stored on disk for extensions that should
 | |
|    * persist menus across sessions.
 | |
|    *
 | |
|    * Used by menus.create API method.
 | |
|    *
 | |
|    * @param {Extension} extension
 | |
|    * @param {object} createProperties The properties for the
 | |
|    * newly created menu.
 | |
|    */
 | |
|   addMenu(extension, createProperties) {
 | |
|     // Only keep properties that are necessary.
 | |
|     const menuProperties = this.mergeMenuProperties({}, createProperties);
 | |
|     return this._getManager(extension).updateMenus(menuProperties);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Update menu data and optionally reparent menus (used when the update to
 | |
|    * a menu includes a different parentId). A DeferredTask scheduled by this
 | |
|    * method will update all menus data stored on disk for extensions that should
 | |
|    * persist menus across sessions.
 | |
|    *
 | |
|    * Used by menus.update API method.
 | |
|    *
 | |
|    * @param {Extension} extension
 | |
|    * @param {MenuId}    menuId           The id of the menu to be updated.
 | |
|    * @param {object}    updateProperties The properties to update on an existing
 | |
|    * menu.
 | |
|    * be reparented.
 | |
|    */
 | |
|   updateMenu(extension, menuId, updateProperties) {
 | |
|     let menuProperties = this.getMenus(extension).get(menuId);
 | |
|     let needsReparenting =
 | |
|       updateProperties.parentId != null &&
 | |
|       menuProperties.parentId != updateProperties.parentId;
 | |
|     // Only keep properties that are necessary.
 | |
|     menuProperties = this.mergeMenuProperties(
 | |
|       this.getMenus(extension).get(menuId),
 | |
|       updateProperties
 | |
|     );
 | |
|     return this._getManager(extension).updateMenus(
 | |
|       menuProperties,
 | |
|       needsReparenting
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Delete the given menu ids. A DeferredTask will update all menus
 | |
|    * data stored on disk for extensions that should persist menus across sessions.
 | |
|    *
 | |
|    * @param {Extension} extension
 | |
|    * @param {Array<MenuId>} menuIds Array of menu ids to remove.
 | |
|    */
 | |
|   deleteMenus(extension, menuIds) {
 | |
|     this._getManager(extension).deleteMenus(menuIds);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Delete all menus. A DeferredTask scheduled by this method will update all menus
 | |
|    * data stored on disk for extensions that should persist menus across sessions.
 | |
|    *
 | |
|    * @param {Extension} extension
 | |
|    */
 | |
|   deleteAllMenus(extension) {
 | |
|     this._getManager(extension).deleteAllMenus();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Remove the entry for the given extensionId from the data stored on disk (if any).
 | |
|    *
 | |
|    * @param {string} extensionId
 | |
|    *
 | |
|    * @returns {Promise<void>} A promise resolved when the extension data has been
 | |
|    * removed from the store.
 | |
|    */
 | |
|   clearPersistedMenusOnUninstall(extensionId) {
 | |
|     return store.clearPersistedMenusOnUninstall(extensionId);
 | |
|   },
 | |
| };
 | 
