/* 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/. */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs", }); export var ClipboardContextMenu = { MENU_POPUP_ID: "clipboardReadPasteMenuPopup", // EventListener interface. handleEvent(aEvent) { switch (aEvent.type) { case "command": { this.onCommand(); break; } case "popuphiding": { this.onPopupHiding(); break; } case "keydown": { this.onKeyDown(aEvent); break; } } }, _pasteMenuItemClicked: false, onCommand() { // onPopupHiding is responsible for returning result by calling onComplete // function. this._pasteMenuItemClicked = true; }, onPopupHiding() { // Remove the listeners before potentially sending the async message // below, because that might throw. this._removeMenupopupEventListeners(); this._clearDelayTimer(); this._stopWatchingForSpammyActivation(); this._menupopup = null; this._menuitem = null; let propBag = lazy.PromptUtils.objectToPropBag({ ok: this._pasteMenuItemClicked, }); this._pendingRequest.resolve(propBag); // A result has already been responded to. Reset the state to properly // handle further click or dismiss events. this._pasteMenuItemClicked = false; this._pendingRequest = null; }, _lastBeepTime: 0, onKeyDown(aEvent) { if (!this._menuitem.disabled) { return; } let accesskey = this._menuitem.getAttribute("accesskey"); if ( aEvent.key == accesskey.toLowerCase() || aEvent.key == accesskey.toUpperCase() ) { if (Date.now() - this._lastBeepTime > 1000) { Cc["@mozilla.org/sound;1"].getService(Ci.nsISound).beep(); this._lastBeepTime = Date.now(); } this._refreshDelayTimer(); } }, _menupopup: null, _menuitem: null, _pendingRequest: null, confirmUserPaste(aWindowContext) { return new Promise((resolve, reject) => { if (!aWindowContext) { reject( Components.Exception("Null window context.", Cr.NS_ERROR_INVALID_ARG) ); return; } let { document } = aWindowContext.browsingContext.topChromeWindow; if (!document) { reject( Components.Exception( "Unable to get chrome document.", Cr.NS_ERROR_FAILURE ) ); return; } if (this._pendingRequest) { reject( Components.Exception( "There is an ongoing request.", Cr.NS_ERROR_FAILURE ) ); return; } this._pendingRequest = { resolve, reject }; this._menupopup = this._getMenupopup(document); this._menuitem = this._menupopup.firstElementChild; this._addMenupopupEventListeners(); let mouseXInCSSPixels = {}; let mouseYInCSSPixels = {}; document.ownerGlobal.windowUtils.getLastOverWindowPointerLocationInCSSPixels( mouseXInCSSPixels, mouseYInCSSPixels ); this._menuitem.disabled = true; this._startWatchingForSpammyActivation(); // `openPopup` is a no-op if the popup is already opened. // That property is used when `navigator.clipboard.readText()` or // `navigator.clipboard.read()`is called from two different frames, e.g. // an iframe and the top level frame. In that scenario, the two frames // correspond to different `navigator.clipboard` instances. When // `readText()` or `read()` is called from both frames, an actor pair is // instantiated for each of them. Both actor parents will call `openPopup` // on the same `_menupopup` object. If the popup is already open, // `openPopup` is a no-op. When the popup is clicked or dismissed both // actor parents will receive the corresponding event. this._menupopup.openPopup( null, "overlap" /* options */, mouseXInCSSPixels.value, mouseYInCSSPixels.value, true /* isContextMenu */ ); this._refreshDelayTimer(document); }); }, _addMenupopupEventListeners() { this._menupopup.addEventListener("command", this); this._menupopup.addEventListener("popuphiding", this); }, _removeMenupopupEventListeners() { this._menupopup.removeEventListener("command", this); this._menupopup.removeEventListener("popuphiding", this); }, _createMenupopup(aChromeDoc) { let menuitem = aChromeDoc.createXULElement("menuitem"); menuitem.id = "clipboardReadPasteMenuItem"; aChromeDoc.l10n.setAttributes(menuitem, "text-action-paste"); let menupopup = aChromeDoc.createXULElement("menupopup"); menupopup.id = this.MENU_POPUP_ID; menupopup.appendChild(menuitem); return menupopup; }, _getMenupopup(aChromeDoc) { let menupopup = aChromeDoc.getElementById(this.MENU_POPUP_ID); if (menupopup == null) { menupopup = this._createMenupopup(aChromeDoc); const parent = aChromeDoc.querySelector("popupset") || aChromeDoc.documentElement; parent.appendChild(menupopup); } return menupopup; }, _startWatchingForSpammyActivation() { let doc = this._menuitem.ownerDocument; doc.addEventListener("keydown", this, { capture: true, mozSystemGroup: true, }); }, _stopWatchingForSpammyActivation() { let doc = this._menuitem.ownerDocument; doc.removeEventListener("keydown", this, { capture: true, mozSystemGroup: true, }); }, _delayTimer: null, _clearDelayTimer() { if (this._delayTimer) { let window = this._menuitem.ownerGlobal; window.clearTimeout(this._delayTimer); this._delayTimer = null; } }, _refreshDelayTimer() { this._clearDelayTimer(); let window = this._menuitem.ownerGlobal; let delay = Services.prefs.getIntPref("security.dialog_enable_delay"); this._delayTimer = window.setTimeout(() => { this._menuitem.disabled = false; this._stopWatchingForSpammyActivation(); this._delayTimer = null; }, delay); }, };