diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /uriloader/exthandler/ExtHandlerService.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'uriloader/exthandler/ExtHandlerService.sys.mjs')
-rw-r--r-- | uriloader/exthandler/ExtHandlerService.sys.mjs | 757 |
1 files changed, 757 insertions, 0 deletions
diff --git a/uriloader/exthandler/ExtHandlerService.sys.mjs b/uriloader/exthandler/ExtHandlerService.sys.mjs new file mode 100644 index 0000000000..aad2c0b8d9 --- /dev/null +++ b/uriloader/exthandler/ExtHandlerService.sys.mjs @@ -0,0 +1,757 @@ +/* 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const { + saveToDisk, + alwaysAsk, + useHelperApp, + handleInternally, + useSystemDefault, +} = Ci.nsIHandlerInfo; + +const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + kHandlerList: "resource://gre/modules/handlers/HandlerList.sys.mjs", + kHandlerListVersion: "resource://gre/modules/handlers/HandlerList.sys.mjs", + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + JSONFile: "resource://gre/modules/JSONFile.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "externalProtocolService", + "@mozilla.org/uriloader/external-protocol-service;1", + "nsIExternalProtocolService" +); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "MIMEService", + "@mozilla.org/mime;1", + "nsIMIMEService" +); + +export function HandlerService() { + // Observe handlersvc-json-replace so we can switch to the datasource + Services.obs.addObserver(this, "handlersvc-json-replace", true); +} + +HandlerService.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsISupportsWeakReference", + "nsIHandlerService", + "nsIObserver", + ]), + + __store: null, + get _store() { + if (!this.__store) { + this.__store = new lazy.JSONFile({ + path: PathUtils.join( + Services.dirsvc.get("ProfD", Ci.nsIFile).path, + "handlers.json" + ), + dataPostProcessor: this._dataPostProcessor.bind(this), + }); + } + + // Always call this even if this.__store was set, since it may have been + // set by asyncInit, which might not have completed yet. + this._ensureStoreInitialized(); + return this.__store; + }, + + __storeInitialized: false, + _ensureStoreInitialized() { + if (!this.__storeInitialized) { + this.__storeInitialized = true; + this.__store.ensureDataReady(); + + this._injectDefaultProtocolHandlersIfNeeded(); + this._migrateProtocolHandlersIfNeeded(); + + Services.obs.notifyObservers(null, "handlersvc-store-initialized"); + } + }, + + _dataPostProcessor(data) { + return data.defaultHandlersVersion + ? data + : { + defaultHandlersVersion: {}, + mimeTypes: {}, + schemes: {}, + isDownloadsImprovementsAlreadyMigrated: false, + }; + }, + + /** + * Injects new default protocol handlers if the version in the preferences is + * newer than the one in the data store. + */ + _injectDefaultProtocolHandlersIfNeeded() { + try { + let defaultHandlersVersion = Services.prefs.getIntPref( + "gecko.handlerService.defaultHandlersVersion", + 0 + ); + if (defaultHandlersVersion < lazy.kHandlerListVersion) { + this._injectDefaultProtocolHandlers(); + Services.prefs.setIntPref( + "gecko.handlerService.defaultHandlersVersion", + lazy.kHandlerListVersion + ); + // Now save the result: + this._store.saveSoon(); + } + } catch (ex) { + console.error(ex); + } + }, + + _injectDefaultProtocolHandlers() { + let locale = Services.locale.appLocaleAsBCP47; + + // Initialize handlers to default and update based on locale. + let localeHandlers = lazy.kHandlerList.default; + if (lazy.kHandlerList[locale]) { + for (let scheme in lazy.kHandlerList[locale].schemes) { + localeHandlers.schemes[scheme] = + lazy.kHandlerList[locale].schemes[scheme]; + } + } + + // Now, we're going to cheat. Terribly. The idiologically correct way + // of implementing the following bit of code would be to fetch the + // handler info objects from the protocol service, manipulate those, + // and then store each of them. + // However, that's expensive. It causes us to talk to the OS about + // default apps, which causes the OS to go hit the disk. + // All we're trying to do is insert some web apps into the list. We + // don't care what's already in the file, we just want to do the + // equivalent of appending into the database. So let's just go do that: + for (let scheme of Object.keys(localeHandlers.schemes)) { + if (scheme == "mailto" && AppConstants.MOZ_APP_NAME == "thunderbird") { + // Thunderbird IS a mailto handler, it doesn't need handlers added. + continue; + } + + let existingSchemeInfo = this._store.data.schemes[scheme]; + if (!existingSchemeInfo) { + // Haven't seen this scheme before. Default to asking which app the + // user wants to use: + existingSchemeInfo = { + // Signal to future readers that we didn't ask the OS anything. + // When the entry is first used, get the info from the OS. + stubEntry: true, + // The first item in the list is the preferred handler, and + // there isn't one, so we fill in null: + handlers: [null], + }; + this._store.data.schemes[scheme] = existingSchemeInfo; + } + let { handlers } = existingSchemeInfo; + for (let newHandler of localeHandlers.schemes[scheme].handlers) { + if (!newHandler.uriTemplate) { + console.error( + `Ignoring protocol handler for ${scheme} without a uriTemplate!` + ); + continue; + } + if (!newHandler.uriTemplate.startsWith("https://")) { + console.error( + `Ignoring protocol handler for ${scheme} with insecure template URL ${newHandler.uriTemplate}.` + ); + continue; + } + if (!newHandler.uriTemplate.toLowerCase().includes("%s")) { + console.error( + `Ignoring protocol handler for ${scheme} with invalid template URL ${newHandler.uriTemplate}.` + ); + continue; + } + // If there is already a handler registered with the same template + // URL, ignore the new one: + let matchingTemplate = handler => + handler && handler.uriTemplate == newHandler.uriTemplate; + if (!handlers.some(matchingTemplate)) { + handlers.push(newHandler); + } + } + } + }, + + /** + * Execute any migrations. Migrations are defined here for any changes or removals for + * existing handlers. Additions are still handled via the localized prefs infrastructure. + * + * This depends on the browser.handlers.migrations pref being set by migrateUI in + * nsBrowserGlue (for Fx Desktop) or similar mechanisms for other products. + * This is a comma-separated list of identifiers of migrations that need running. + * This avoids both re-running older migrations and keeping an additional + * pref around permanently. + */ + _migrateProtocolHandlersIfNeeded() { + const kMigrations = { + "30boxes": () => { + const k30BoxesRegex = + /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i; + let webcalHandler = + lazy.externalProtocolService.getProtocolHandlerInfo("webcal"); + if (this.exists(webcalHandler)) { + this.fillHandlerInfo(webcalHandler, ""); + let shouldStore = false; + // First remove 30boxes from possible handlers. + let handlers = webcalHandler.possibleApplicationHandlers; + for (let i = handlers.length - 1; i >= 0; i--) { + let app = handlers.queryElementAt(i, Ci.nsIHandlerApp); + if ( + app instanceof Ci.nsIWebHandlerApp && + k30BoxesRegex.test(app.uriTemplate) + ) { + shouldStore = true; + handlers.removeElementAt(i); + } + } + // Then remove as a preferred handler. + if (webcalHandler.preferredApplicationHandler) { + let app = webcalHandler.preferredApplicationHandler; + if ( + app instanceof Ci.nsIWebHandlerApp && + k30BoxesRegex.test(app.uriTemplate) + ) { + webcalHandler.preferredApplicationHandler = null; + shouldStore = true; + } + } + // Then store, if we changed anything. + if (shouldStore) { + this.store(webcalHandler); + } + } + }, + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1526890 for context. + "secure-mail": () => { + const kSubstitutions = new Map([ + [ + "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s", + "https://mail.yahoo.co.jp/compose/?To=%s", + ], + [ + "http://www.inbox.lv/rfc2368/?value=%s", + "https://mail.inbox.lv/compose?to=%s", + ], + [ + "http://poczta.interia.pl/mh/?mailto=%s", + "https://poczta.interia.pl/mh/?mailto=%s", + ], + [ + "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s", + "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s", + ], + ]); + + function maybeReplaceURL(app) { + if (app instanceof Ci.nsIWebHandlerApp) { + let { uriTemplate } = app; + let sub = kSubstitutions.get(uriTemplate); + if (sub) { + app.uriTemplate = sub; + return true; + } + } + return false; + } + let mailHandler = + lazy.externalProtocolService.getProtocolHandlerInfo("mailto"); + if (this.exists(mailHandler)) { + this.fillHandlerInfo(mailHandler, ""); + let handlers = mailHandler.possibleApplicationHandlers; + let shouldStore = false; + for (let i = handlers.length - 1; i >= 0; i--) { + let app = handlers.queryElementAt(i, Ci.nsIHandlerApp); + // Note: will evaluate the RHS because it's a binary rather than + // logical or. + shouldStore |= maybeReplaceURL(app); + } + // Then check the preferred handler. + if (mailHandler.preferredApplicationHandler) { + shouldStore |= maybeReplaceURL( + mailHandler.preferredApplicationHandler + ); + } + // Then store, if we changed anything. Note that store() handles + // duplicates, so we don't have to. + if (shouldStore) { + this.store(mailHandler); + } + } + }, + }; + let migrationsToRun = Services.prefs.getCharPref( + "browser.handlers.migrations", + "" + ); + migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : []; + for (let migration of migrationsToRun) { + migration.trim(); + try { + kMigrations[migration](); + } catch (ex) { + console.error(ex); + } + } + + if (migrationsToRun.length) { + Services.prefs.clearUserPref("browser.handlers.migrations"); + } + }, + + _onDBChange() { + return (async () => { + if (this.__store) { + await this.__store.finalize(); + } + this.__store = null; + this.__storeInitialized = false; + })().catch(console.error); + }, + + // nsIObserver + observe(subject, topic, data) { + if (topic != "handlersvc-json-replace") { + return; + } + let promise = this._onDBChange(); + promise.then(() => { + Services.obs.notifyObservers(null, "handlersvc-json-replace-complete"); + }); + }, + + // nsIHandlerService + asyncInit() { + if (!this.__store) { + this.__store = new lazy.JSONFile({ + path: PathUtils.join( + Services.dirsvc.get("ProfD", Ci.nsIFile).path, + "handlers.json" + ), + dataPostProcessor: this._dataPostProcessor.bind(this), + }); + this.__store + .load() + .then(() => { + // __store can be null if we called _onDBChange in the mean time. + if (this.__store) { + this._ensureStoreInitialized(); + } + }) + .catch(console.error); + } + }, + + // nsIHandlerService + enumerate() { + let handlers = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray + ); + for (let [type, typeInfo] of Object.entries(this._store.data.mimeTypes)) { + let primaryExtension = typeInfo.extensions?.[0] ?? null; + let handler = lazy.MIMEService.getFromTypeAndExtension( + type, + primaryExtension + ); + handlers.appendElement(handler); + } + for (let type of Object.keys(this._store.data.schemes)) { + // nsIExternalProtocolService.getProtocolHandlerInfo can be expensive + // on Windows, so we return a proxy to delay retrieving the nsIHandlerInfo + // until one of its properties is accessed. + // + // Note: our caller still needs to yield periodically when iterating + // the enumerator and accessing handler properties to avoid monopolizing + // the main thread. + // + let handler = new Proxy( + { + QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]), + type, + get _handlerInfo() { + delete this._handlerInfo; + return (this._handlerInfo = + lazy.externalProtocolService.getProtocolHandlerInfo(type)); + }, + }, + { + get(target, name) { + return target[name] || target._handlerInfo[name]; + }, + set(target, name, value) { + target._handlerInfo[name] = value; + }, + } + ); + handlers.appendElement(handler); + } + return handlers.enumerate(Ci.nsIHandlerInfo); + }, + + // nsIHandlerService + store(handlerInfo) { + let handlerList = this._getHandlerListByHandlerInfoType(handlerInfo); + + // Retrieve an existing entry if present, instead of creating a new one, so + // that we preserve unknown properties for forward compatibility. + let storedHandlerInfo = handlerList[handlerInfo.type]; + if (!storedHandlerInfo) { + storedHandlerInfo = {}; + handlerList[handlerInfo.type] = storedHandlerInfo; + } + + // Only a limited number of preferredAction values is allowed. + if ( + handlerInfo.preferredAction == saveToDisk || + handlerInfo.preferredAction == useSystemDefault || + handlerInfo.preferredAction == handleInternally || + // For files (ie mimetype rather than protocol handling info), ensure + // we can store the "always ask" state, too: + (handlerInfo.preferredAction == alwaysAsk && + this._isMIMEInfo(handlerInfo)) + ) { + storedHandlerInfo.action = handlerInfo.preferredAction; + } else { + storedHandlerInfo.action = useHelperApp; + } + + if (handlerInfo.alwaysAskBeforeHandling) { + storedHandlerInfo.ask = true; + } else { + delete storedHandlerInfo.ask; + } + + // Build a list of unique nsIHandlerInfo instances to process later. + let handlers = []; + if (handlerInfo.preferredApplicationHandler) { + handlers.push(handlerInfo.preferredApplicationHandler); + } + for (let handler of handlerInfo.possibleApplicationHandlers.enumerate( + Ci.nsIHandlerApp + )) { + // If the caller stored duplicate handlers, we save them only once. + if (!handlers.some(h => h.equals(handler))) { + handlers.push(handler); + } + } + + // If any of the nsIHandlerInfo instances cannot be serialized, it is not + // included in the final list. The first element is always the preferred + // handler, or null if there is none. + let serializableHandlers = handlers + .map(h => this.handlerAppToSerializable(h)) + .filter(h => h); + if (serializableHandlers.length) { + if (!handlerInfo.preferredApplicationHandler) { + serializableHandlers.unshift(null); + } + storedHandlerInfo.handlers = serializableHandlers; + } else { + delete storedHandlerInfo.handlers; + } + + if (this._isMIMEInfo(handlerInfo)) { + let extensions = storedHandlerInfo.extensions || []; + for (let extension of handlerInfo.getFileExtensions()) { + extension = extension.toLowerCase(); + // If the caller stored duplicate extensions, we save them only once. + if (!extensions.includes(extension)) { + extensions.push(extension); + } + } + if (extensions.length) { + storedHandlerInfo.extensions = extensions; + } else { + delete storedHandlerInfo.extensions; + } + } + + // If we're saving *anything*, it stops being a stub: + delete storedHandlerInfo.stubEntry; + + this._store.saveSoon(); + + // Now notify PDF.js. This is hacky, but a lot better than expecting all + // the consumers to do it... + if (handlerInfo.type == "application/pdf") { + Services.obs.notifyObservers(null, TOPIC_PDFJS_HANDLER_CHANGED); + } + }, + + // nsIHandlerService + fillHandlerInfo(handlerInfo, overrideType) { + let type = overrideType || handlerInfo.type; + let storedHandlerInfo = + this._getHandlerListByHandlerInfoType(handlerInfo)[type]; + if (!storedHandlerInfo) { + throw new Components.Exception( + "handlerSvc fillHandlerInfo: don't know this type", + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + + let isStub = !!storedHandlerInfo.stubEntry; + // In the normal case, this is not a stub, so we can just read stored info + // and write to the handlerInfo object we were passed. + if (!isStub) { + handlerInfo.preferredAction = storedHandlerInfo.action; + handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask; + } else { + // If we've got a stub, ensure the defaults are still set: + lazy.externalProtocolService.setProtocolHandlerDefaults( + handlerInfo, + handlerInfo.hasDefaultHandler + ); + if ( + handlerInfo.preferredAction == alwaysAsk && + handlerInfo.alwaysAskBeforeHandling + ) { + // `store` will default to `useHelperApp` because `alwaysAsk` is + // not one of the 3 recognized options; for compatibility, do + // the same here. + handlerInfo.preferredAction = useHelperApp; + } + } + // If it *is* a stub, don't override alwaysAskBeforeHandling or the + // preferred actions. Instead, just append the stored handlers, without + // overriding the preferred app, and then schedule a task to store proper + // info for this handler. + this._appendStoredHandlers(handlerInfo, storedHandlerInfo.handlers, isStub); + + if (this._isMIMEInfo(handlerInfo) && storedHandlerInfo.extensions) { + for (let extension of storedHandlerInfo.extensions) { + handlerInfo.appendExtension(extension); + } + } else if (this._mockedHandler) { + this._insertMockedHandler(handlerInfo); + } + }, + + /** + * Private method to inject stored handler information into an nsIHandlerInfo + * instance. + * @param handlerInfo the nsIHandlerInfo instance to write to + * @param storedHandlers the stored handlers + * @param keepPreferredApp whether to keep the handlerInfo's + * preferredApplicationHandler or override it + * (default: false, ie override it) + */ + _appendStoredHandlers(handlerInfo, storedHandlers, keepPreferredApp) { + // If the first item is not null, it is also the preferred handler. Since + // we cannot modify the stored array, use a boolean to keep track of this. + let isFirstItem = true; + for (let handler of storedHandlers || [null]) { + let handlerApp = this.handlerAppFromSerializable(handler || {}); + if (isFirstItem) { + isFirstItem = false; + // Do not overwrite the preferred app unless that's allowed + if (!keepPreferredApp) { + handlerInfo.preferredApplicationHandler = handlerApp; + } + } + if (handlerApp) { + handlerInfo.possibleApplicationHandlers.appendElement(handlerApp); + } + } + }, + + /** + * @param handler + * A nsIHandlerApp handler app + * @returns Serializable representation of a handler app object. + */ + handlerAppToSerializable(handler) { + if (handler instanceof Ci.nsILocalHandlerApp) { + return { + name: handler.name, + path: handler.executable.path, + }; + } else if (handler instanceof Ci.nsIWebHandlerApp) { + return { + name: handler.name, + uriTemplate: handler.uriTemplate, + }; + } else if (handler instanceof Ci.nsIDBusHandlerApp) { + return { + name: handler.name, + service: handler.service, + method: handler.method, + objectPath: handler.objectPath, + dBusInterface: handler.dBusInterface, + }; + } else if (handler instanceof Ci.nsIGIOMimeApp) { + return { + name: handler.name, + command: handler.command, + }; + } + // If the handler is an unknown handler type, return null. + // Android default application handler is the case. + return null; + }, + + /** + * @param handlerObj + * Serializable representation of a handler object. + * @returns {nsIHandlerApp} the handler app, if any; otherwise null + */ + handlerAppFromSerializable(handlerObj) { + let handlerApp; + if ("path" in handlerObj) { + try { + let file = new lazy.FileUtils.File(handlerObj.path); + if (!file.exists()) { + return null; + } + handlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + handlerApp.executable = file; + } catch (ex) { + return null; + } + } else if ("uriTemplate" in handlerObj) { + handlerApp = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(Ci.nsIWebHandlerApp); + handlerApp.uriTemplate = handlerObj.uriTemplate; + } else if ("service" in handlerObj) { + handlerApp = Cc[ + "@mozilla.org/uriloader/dbus-handler-app;1" + ].createInstance(Ci.nsIDBusHandlerApp); + handlerApp.service = handlerObj.service; + handlerApp.method = handlerObj.method; + handlerApp.objectPath = handlerObj.objectPath; + handlerApp.dBusInterface = handlerObj.dBusInterface; + } else if ("command" in handlerObj && "@mozilla.org/gio-service;1" in Cc) { + try { + handlerApp = Cc["@mozilla.org/gio-service;1"] + .getService(Ci.nsIGIOService) + .createAppFromCommand(handlerObj.command, handlerObj.name); + } catch (ex) { + return null; + } + } else { + return null; + } + + handlerApp.name = handlerObj.name; + return handlerApp; + }, + + /** + * The function returns a reference to the "mimeTypes" or "schemes" object + * based on which type of handlerInfo is provided. + */ + _getHandlerListByHandlerInfoType(handlerInfo) { + return this._isMIMEInfo(handlerInfo) + ? this._store.data.mimeTypes + : this._store.data.schemes; + }, + + /** + * Determines whether an nsIHandlerInfo instance represents a MIME type. + */ + _isMIMEInfo(handlerInfo) { + // We cannot rely only on the instanceof check because on Android both MIME + // types and protocols are instances of nsIMIMEInfo. We still do the check + // so that properties of nsIMIMEInfo become available to the callers. + return ( + handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/") + ); + }, + + // nsIHandlerService + exists(handlerInfo) { + return ( + handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo) + ); + }, + + // nsIHandlerService + remove(handlerInfo) { + delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type]; + this._store.saveSoon(); + }, + + // nsIHandlerService + getTypeFromExtension(fileExtension) { + let extension = fileExtension.toLowerCase(); + let mimeTypes = this._store.data.mimeTypes; + for (let type of Object.keys(mimeTypes)) { + if ( + mimeTypes[type].extensions && + mimeTypes[type].extensions.includes(extension) + ) { + return type; + } + } + return ""; + }, + + _mockedHandler: null, + _mockedProtocol: null, + + _insertMockedHandler(handlerInfo) { + if (handlerInfo.type == this._mockedProtocol) { + handlerInfo.preferredApplicationHandler = this._mockedHandler; + handlerInfo.possibleApplicationHandlers.insertElementAt( + this._mockedHandler, + 0 + ); + } + }, + + // test-only: mock the handler instance for a particular protocol/scheme + mockProtocolHandler(protocol) { + if (!protocol) { + this._mockedProtocol = null; + this._mockedHandler = null; + return; + } + this._mockedProtocol = protocol; + this._mockedHandler = { + QueryInterface: ChromeUtils.generateQI([Ci.nsILocalHandlerApp]), + launchWithURI(uri, context) { + Services.obs.notifyObservers(uri, "mocked-protocol-handler"); + }, + name: "Mocked handler", + detailedDescription: "Mocked handler for tests", + equals(x) { + return x == this; + }, + get executable() { + if (AppConstants.platform == "macosx") { + // We need an app path that isn't us, nor in our app bundle, and + // Apple no longer allows us to read the default-shipped apps + // in /Applications/ - except for Safari, it would appear! + let f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + f.initWithPath("/Applications/Safari.app"); + return f; + } + return Services.dirsvc.get("XCurProcD", Ci.nsIFile); + }, + parameterCount: 0, + clearParameters() {}, + appendParameter() {}, + getParameter() {}, + parameterExists() { + return false; + }, + }; + }, +}; |