From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/mail/components/MessengerContentHandler.jsm | 793 +++++++++++++++++++++++ 1 file changed, 793 insertions(+) create mode 100644 comm/mail/components/MessengerContentHandler.jsm (limited to 'comm/mail/components/MessengerContentHandler.jsm') diff --git a/comm/mail/components/MessengerContentHandler.jsm b/comm/mail/components/MessengerContentHandler.jsm new file mode 100644 index 0000000000..06d37a0811 --- /dev/null +++ b/comm/mail/components/MessengerContentHandler.jsm @@ -0,0 +1,793 @@ +/* -*- indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +var EXPORTED_SYMBOLS = [ + "MessengerContentHandler", + "MessageDisplayContentHandler", +]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + FeedUtils: "resource:///modules/FeedUtils.jsm", + MailUtils: "resource:///modules/MailUtils.jsm", + MimeParser: "resource:///modules/mimeParser.jsm", + NetUtil: "resource://gre/modules/NetUtil.jsm", +}); + +function resolveURIInternal(aCmdLine, aArgument) { + var uri = aCmdLine.resolveURI(aArgument); + + if (!(uri instanceof Ci.nsIFileURL)) { + return uri; + } + + try { + if (uri.file.exists()) { + return uri; + } + } catch (e) { + console.error(e); + } + + // We have interpreted the argument as a relative file URI, but the file + // doesn't exist. Try URI fixup heuristics: see bug 290782. + + try { + uri = Services.uriFixup.getFixupURIInfo(aArgument, 0).preferredURI; + } catch (e) { + console.error(e); + } + + return uri; +} + +function handleIndexerResult(aFile) { + // Do this here because xpcshell isn't too happy with this at startup + // Make sure the folder tree is initialized + lazy.MailUtils.discoverFolders(); + + // Use the search integration module to convert the indexer result into a + // message header + const { SearchIntegration } = ChromeUtils.import( + "resource:///modules/SearchIntegration.jsm" + ); + let msgHdr = SearchIntegration.handleResult(aFile); + + // If we found a message header, open it, otherwise throw an exception + if (msgHdr) { + getOrOpen3PaneWindow().then(win => { + lazy.MailUtils.displayMessage(msgHdr); + }); + } else { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } +} + +async function getOrOpen3PaneWindow() { + let win = Services.wm.getMostRecentWindow("mail:3pane"); + + if (!win) { + const startupPromise = new Promise(resolve => { + Services.obs.addObserver( + { + observe(subject) { + if (subject == win) { + Services.obs.removeObserver(this, "mail-startup-done"); + resolve(); + } + }, + }, + "mail-startup-done" + ); + }); + + // Bug 277798 - we have to pass an argument to openWindow(), or + // else it won't honor the dialog=no instruction. + const argstring = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + win = Services.ww.openWindow( + null, + "chrome://messenger/content/messenger.xhtml", + "_blank", + "chrome,dialog=no,all", + argstring + ); + await startupPromise; + } + + await win.delayedStartupPromise; + return win; +} + +/** + * Open the given uri. + * @param {nsIURI} uri - The uri to open. + */ +function openURI(uri) { + if ( + !Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService) + .isExposedProtocol(uri.scheme) + ) { + throw Components.Exception(`Can't open: ${uri.spec}`, Cr.NS_ERROR_FAILURE); + } + + var channel = Services.io.newChannelFromURI( + uri, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + var loader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader); + + // We cannot load a URI on startup asynchronously without protecting + // the startup + + var loadgroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + + var loadlistener = { + onStartRequest(aRequest) { + Services.startup.enterLastWindowClosingSurvivalArea(); + }, + + onStopRequest(aRequest, aStatusCode) { + Services.startup.exitLastWindowClosingSurvivalArea(); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIRequestObserver", + "nsISupportsWeakReference", + ]), + }; + + loadgroup.groupObserver = loadlistener; + + var listener = { + doContent(ctype, preferred, request, handler) { + var newHandler = Cc[ + "@mozilla.org/uriloader/content-handler;1?type=application/x-message-display" + ].createInstance(Ci.nsIContentHandler); + newHandler.handleContent("application/x-message-display", this, request); + return true; + }, + isPreferred(ctype, desired) { + if (ctype == "message/rfc822") { + return true; + } + return false; + }, + canHandleContent(ctype, preferred, desired) { + return false; + }, + loadCookie: null, + parentContentListener: null, + getInterface(iid) { + if (iid.equals(Ci.nsIURIContentListener)) { + return this; + } + + if (iid.equals(Ci.nsILoadGroup)) { + return loadgroup; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + }; + loader.openURI(channel, true, listener); +} + +function MailDefaultHandler() {} + +MailDefaultHandler.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsICommandLineHandler", + "nsICommandLineValidator", + "nsIFactory", + ]), + + /* nsICommandLineHandler */ + + handle(cmdLine) { + if ( + cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH && + Services.startup.wasSilentlyStarted + ) { + // If we are starting up in silent mode, don't open a window. We also need + // to make sure that the application doesn't immediately exit, so stay in + // a LastWindowClosingSurvivalArea until a window opens. + Services.startup.enterLastWindowClosingSurvivalArea(); + Services.obs.addObserver(function windowOpenObserver() { + Services.startup.exitLastWindowClosingSurvivalArea(); + Services.obs.removeObserver(windowOpenObserver, "domwindowopened"); + }, "domwindowopened"); + return; + } + + try { + var remoteCommand = cmdLine.handleFlagWithParam("remote", true); + } catch (e) { + throw Components.Exception("", Cr.NS_ERROR_ABORT); + } + + if (remoteCommand != null) { + try { + var a = /^\s*(\w+)\(([^\)]*)\)\s*$/.exec(remoteCommand); + var remoteVerb = a[1].toLowerCase(); + var remoteParams = a[2].split(","); + + switch (remoteVerb) { + case "openurl": { + let xuri = cmdLine.resolveURI(remoteParams[0]); + openURI(xuri); + break; + } + case "mailto": { + let xuri = cmdLine.resolveURI("mailto:" + remoteParams[0]); + openURI(xuri); + break; + } + case "xfedocommand": + // xfeDoCommand(openBrowser) + switch (remoteParams[0].toLowerCase()) { + case "openinbox": { + getOrOpen3PaneWindow().then(win => win.focus()); + break; + } + case "composemessage": { + let argstring = Cc[ + "@mozilla.org/supports-string;1" + ].createInstance(Ci.nsISupportsString); + remoteParams.shift(); + argstring.data = remoteParams.join(","); + let args = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray + ); + args.appendElement(argstring); + args.appendElement(cmdLine); + getOrOpen3PaneWindow().then(win => + Services.ww.openWindow( + win, + "chrome://messenger/content/messengercompose/messengercompose.xhtml", + "_blank", + "chrome,dialog=no,all", + args + ) + ); + break; + } + default: + throw Components.Exception("", Cr.NS_ERROR_ABORT); + } + break; + + default: + // Somebody sent us a remote command we don't know how to process: + // just abort. + throw Components.Exception( + `Unrecognized command: ${remoteParams[0]}`, + Cr.NS_ERROR_ABORT + ); + } + + cmdLine.preventDefault = true; + } catch (e) { + // If we had a -remote flag but failed to process it, throw + // NS_ERROR_ABORT so that the xremote code knows to return a failure + // back to the handling code. + dump(e); + throw Components.Exception("", Cr.NS_ERROR_ABORT); + } + } + + var chromeParam = cmdLine.handleFlagWithParam("chrome", false); + if (chromeParam) { + // The parameter specifies the window to open. This code should *not* + // open messenger.xhtml as well. + try { + let argstring = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + let _uri = resolveURIInternal(cmdLine, chromeParam); + // only load URIs which do not inherit chrome privs + if ( + !Services.io.URIChainHasFlags( + _uri, + Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT + ) + ) { + Services.ww.openWindow( + null, + _uri.spec, + "_blank", + "chrome,dialog=no,all", + argstring + ); + cmdLine.preventDefault = true; + } + } catch (e) { + dump(e); + } + } + + if (cmdLine.handleFlag("silent", false)) { + cmdLine.preventDefault = true; + } + + // -MapiStartup + // indicates that this startup is due to MAPI. Don't do anything for now. + cmdLine.handleFlag("MapiStartup", false); + + if (cmdLine.handleFlag("mail", false)) { + getOrOpen3PaneWindow().then(win => win.focusOnMail(0)); + cmdLine.preventDefault = true; + } + + if (cmdLine.handleFlag("addressbook", false)) { + getOrOpen3PaneWindow().then(win => win.toAddressBook()); + cmdLine.preventDefault = true; + } + + if (cmdLine.handleFlag("options", false)) { + getOrOpen3PaneWindow().then(win => win.openPreferencesTab()); + cmdLine.preventDefault = true; + } + + if (cmdLine.handleFlag("calendar", false)) { + getOrOpen3PaneWindow().then(win => win.toCalendar()); + cmdLine.preventDefault = true; + } + + if (cmdLine.handleFlag("keymanager", false)) { + getOrOpen3PaneWindow().then(win => win.openKeyManager()); + cmdLine.preventDefault = true; + } + + if (cmdLine.handleFlag("setDefaultMail", false)) { + var shell = Cc["@mozilla.org/mail/shell-service;1"].getService( + Ci.nsIShellService + ); + shell.setDefaultClient(true, Ci.nsIShellService.MAIL); + } + + // The URI might be passed as the argument to the file parameter + let uri = cmdLine.handleFlagWithParam("file", false); + // macOS passes `-url mid:` into the command line, drop the -url flag. + cmdLine.handleFlag("url", false); + + var count = cmdLine.length; + if (count) { + var i = 0; + while (i < count) { + var curarg = cmdLine.getArgument(i); + if (!curarg.startsWith("-")) { + break; + } + + dump("Warning: unrecognized command line flag " + curarg + "\n"); + // To emulate the pre-nsICommandLine behavior, we ignore the + // argument after an unrecognized flag. + i += 2; + // xxxbsmedberg: make me use the console service! + } + + if (i < count) { + uri = cmdLine.getArgument(i); + + // mailto: URIs are frequently passed with spaces in them. They should be + // escaped into %20, but we hack around bad clients, see bug 231032 + if (uri.startsWith("mailto:")) { + while (++i < count) { + var testarg = cmdLine.getArgument(i); + if (testarg.startsWith("-")) { + break; + } + + uri += " " + testarg; + } + } + } + } + + if (!uri && cmdLine.preventDefault) { + return; + } + + if (!uri && cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH) { + try { + for (let window of Services.wm.getEnumerator("mail:3pane")) { + window.focus(); + return; + } + } catch (e) { + dump(e); + } + } + if (uri) { + if (/^file:/i.test(uri)) { + // Turn file URL into a file path so `resolveFile()` will work. + let fileURL = cmdLine.resolveURI(uri); + uri = fileURL.QueryInterface(Ci.nsIFileURL).file.path; + } + // Check for protocols first then look at the file ending. + // Protocols are able to contain file endings like '.ics'. + if (/^https?:/i.test(uri) || /^feed:/i.test(uri)) { + getOrOpen3PaneWindow().then(win => { + lazy.FeedUtils.subscribeToFeed(uri, null); + }); + } else if (/^webcals?:\/\//i.test(uri)) { + getOrOpen3PaneWindow().then(win => + Services.ww.openWindow( + win, + "chrome://calendar/content/calendar-creation.xhtml", + "_blank", + "chrome,titlebar,modal,centerscreen", + Services.io.newURI(uri) + ) + ); + } else if (/^mid:/i.test(uri)) { + getOrOpen3PaneWindow().then(win => { + lazy.MailUtils.openMessageByMessageId(uri.slice(4)); + }); + } else if (/^(mailbox|imap|news)-message:\/\//.test(uri)) { + getOrOpen3PaneWindow().then(win => { + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + lazy.MailUtils.displayMessage(messenger.msgHdrFromURI(uri)); + }); + } else if (/^imap:/i.test(uri) || /^s?news:/i.test(uri)) { + getOrOpen3PaneWindow().then(win => { + openURI(cmdLine.resolveURI(uri)); + }); + } else if ( + // While the leading web+ and ext+ identifiers may be case insensitive, + // the protocol identifiers must be lowercase. + /^(web|ext)\+[a-z]+:/i.test(uri) && + /^[a-z]+:/.test(uri.split("+")[1]) + ) { + getOrOpen3PaneWindow().then(win => { + win.gTabmail.openTab("contentTab", { + url: uri, + linkHandler: "single-site", + background: false, + duplicate: true, + }); + }); + } else if ( + uri.toLowerCase().endsWith(".mozeml") || + uri.toLowerCase().endsWith(".wdseml") + ) { + handleIndexerResult(cmdLine.resolveFile(uri)); + cmdLine.preventDefault = true; + } else if (uri.toLowerCase().endsWith(".eml")) { + // Open this eml in a new message window + let file = cmdLine.resolveFile(uri); + // No point in trying to open a file if it doesn't exist or is empty + if (file.exists() && file.fileSize > 0) { + // Read this eml and extract its headers to check for X-Unsent. + let fstream = null; + let headers = new Map(); + try { + fstream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fstream.init(file, -1, 0, 0); + let data = lazy.NetUtil.readInputStreamToString( + fstream, + fstream.available() + ); + headers = lazy.MimeParser.extractHeaders(data); + } catch (e) { + // Ignore errors on reading the eml or extracting its headers. The + // test for the X-Unsent header below will fail and the message + // window will take care of any error handling. + } finally { + if (fstream) { + fstream.close(); + } + } + + // Get the URL for this file + let fileURL = Services.io + .newFileURI(file) + .QueryInterface(Ci.nsIFileURL); + fileURL = fileURL + .mutate() + .setQuery("type=application/x-message-display") + .finalize(); + + if (headers.get("X-Unsent") == "1") { + getOrOpen3PaneWindow().then(win => { + const msgWindow = Cc[ + "@mozilla.org/messenger/msgwindow;1" + ].createInstance(Ci.nsIMsgWindow); + MailServices.compose.OpenComposeWindow( + win, + {}, + fileURL.spec, + Ci.nsIMsgCompType.Draft, + Ci.nsIMsgCompFormat.Default, + null, + headers.get("from"), + msgWindow + ); + }); + } else { + getOrOpen3PaneWindow().then(win => + Services.ww.openWindow( + win, + "chrome://messenger/content/messageWindow.xhtml", + "_blank", + "all,chrome,dialog=no,status,toolbar", + fileURL + ) + ); + } + cmdLine.preventDefault = true; + } else { + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" + ); + let title, message; + if (!file.exists()) { + title = bundle.GetStringFromName("fileNotFoundTitle"); + message = bundle.formatStringFromName("fileNotFoundMsg", [ + file.path, + ]); + } else { + // The file is empty + title = bundle.GetStringFromName("fileEmptyTitle"); + message = bundle.formatStringFromName("fileEmptyMsg", [file.path]); + } + + Services.prompt.alert(null, title, message); + } + } else if (uri.toLowerCase().endsWith(".ics")) { + // An .ics calendar file! Open the ics file dialog. + let file = cmdLine.resolveFile(uri); + if (file.exists() && file.fileSize > 0) { + getOrOpen3PaneWindow().then(win => + Services.ww.openWindow( + win, + "chrome://calendar/content/calendar-ics-file-dialog.xhtml", + "_blank", + "chrome,titlebar,modal,centerscreen", + file + ) + ); + } + } else if (uri.toLowerCase().endsWith(".vcf")) { + // A VCard! Be smart and open the "add contact" dialog. + let file = cmdLine.resolveFile(uri); + if (file.exists() && file.fileSize > 0) { + let winPromise = getOrOpen3PaneWindow(); + let uriSpec = Services.io.newFileURI(file).spec; + lazy.NetUtil.asyncFetch( + { uri: uriSpec, loadUsingSystemPrincipal: true }, + function (inputStream, status) { + if (!Components.isSuccessCode(status)) { + return; + } + + let data = lazy.NetUtil.readInputStreamToString( + inputStream, + inputStream.available() + ); + // Try to detect the character set and decode. Only UTF-8 is + // valid from vCard 4.0, but we support older versions, so other + // charsets are possible. + let charset = Cc["@mozilla.org/messengercompose/computils;1"] + .createInstance(Ci.nsIMsgCompUtils) + .detectCharset(data); + let buffer = new Uint8Array( + Array.from(data, c => c.charCodeAt(0)) + ); + data = new TextDecoder(charset).decode(buffer); + + winPromise.then(win => + win.toAddressBook({ + action: "create", + vCard: decodeURIComponent(data), + }) + ); + } + ); + } + } else { + getOrOpen3PaneWindow().then(win => { + // This must be a regular filename. Use it to create a new message + // with attachment. + let msgParams = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + let composeFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + let localFile = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + let fileHandler = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + + try { + // Unescape the URI so that we work with clients that escape spaces. + localFile.initWithPath(unescape(uri)); + attachment.url = fileHandler.getURLSpecFromActualFile(localFile); + composeFields.addAttachment(attachment); + + msgParams.type = Ci.nsIMsgCompType.New; + msgParams.format = Ci.nsIMsgCompFormat.Default; + msgParams.composeFields = composeFields; + + MailServices.compose.OpenComposeWindowWithParams(win, msgParams); + } catch (e) { + // Let protocol handlers try to take care. + openURI(cmdLine.resolveURI(uri)); + } + }); + } + } else { + getOrOpen3PaneWindow(); + } + }, + + /* nsICommandLineValidator */ + validate(cmdLine) { + var osintFlagIdx = cmdLine.findFlag("osint", false); + if (osintFlagIdx == -1) { + return; + } + + // Other handlers may use osint so only handle the osint flag if the mail + // or compose flag is also present and the command line is valid. + var mailFlagIdx = cmdLine.findFlag("mail", false); + var composeFlagIdx = cmdLine.findFlag("compose", false); + if (mailFlagIdx == -1 && composeFlagIdx == -1) { + return; + } + + // If both flags are present use the first flag found so the command line + // length test will fail. + if (mailFlagIdx > -1 && composeFlagIdx > -1) { + var actionFlagIdx = + mailFlagIdx > composeFlagIdx ? composeFlagIdx : mailFlagIdx; + } else { + actionFlagIdx = mailFlagIdx > -1 ? mailFlagIdx : composeFlagIdx; + } + + if (actionFlagIdx && osintFlagIdx > -1) { + var param = cmdLine.getArgument(actionFlagIdx + 1); + if ( + cmdLine.length != actionFlagIdx + 2 || + /thunderbird.url.(mailto|news):/.test(param) + ) { + throw Components.Exception("", Cr.NS_ERROR_ABORT); + } + cmdLine.handleFlag("osint", false); + } + }, + + openInExternal(uri) { + Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService) + .loadURI(uri); + }, + + handleContent(aContentType, aWindowContext, aRequest) { + try { + if ( + !Cc["@mozilla.org/webnavigation-info;1"] + .getService(Ci.nsIWebNavigationInfo) + .isTypeSupported(aContentType, null) + ) { + throw Components.Exception("", Cr.NS_ERROR_WONT_HANDLE_CONTENT); + } + } catch (e) { + throw Components.Exception("", Cr.NS_ERROR_WONT_HANDLE_CONTENT); + } + + aRequest.QueryInterface(Ci.nsIChannel); + + // For internal protocols (e.g. imap, mailbox, mailto), we want to handle + // them internally as we know what to do. For http and https we don't + // actually deal with external windows very well, so we redirect them to + // the external browser. + if (!aRequest.URI.schemeIs("http") && !aRequest.URI.schemeIs("https")) { + throw Components.Exception("", Cr.NS_ERROR_WONT_HANDLE_CONTENT); + } + + this.openInExternal(aRequest.URI); + aRequest.cancel(Cr.NS_BINDING_ABORTED); + }, + + helpInfo: + " -mail Go to the mail tab.\n" + + " -addressbook Go to the address book tab.\n" + + " -calendar Go to the calendar tab.\n" + + " -options Go to the settings tab.\n" + + " -file Open the specified email file or ICS calendar file.\n" + + " -setDefaultMail Set this app as the default mail client.\n" + + " -keymanager Open the OpenPGP Key Manager.\n", + + /* nsIFactory */ + + createInstance(iid) { + return this.QueryInterface(iid); + }, +}; + +function MessengerContentHandler() { + if (!gMessengerContentHandler) { + gMessengerContentHandler = this; + } + return gMessengerContentHandler; +} + +MessengerContentHandler.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIContentHandler"]), +}; + +var gMessengerContentHandler = new MailDefaultHandler(); + +/** + * Open a message/rfc822 or eml file in a new msg window. + * + * @implements {nsIContentHandler} + */ +class MessageDisplayContentHandler { + QueryInterface = ChromeUtils.generateQI(["nsIContentHandler"]); + + handleContent(contentType, windowContext, request) { + let channel = request.QueryInterface(Ci.nsIChannel); + if (!channel) { + throw Components.Exception( + "Expecting an nsIChannel", + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + let uri = channel.URI; + let mailnewsUrl; + try { + mailnewsUrl = uri.QueryInterface(Ci.nsIMsgMailNewsUrl); + } catch (e) {} + if (mailnewsUrl) { + let queryPart = mailnewsUrl.query.replace( + "type=message/rfc822", + "type=application/x-message-display" + ); + uri = mailnewsUrl.mutate().setQuery(queryPart).finalize(); + } else if (uri.scheme == "file") { + uri = uri + .mutate() + .setQuery("type=application/x-message-display") + .finalize(); + } + getOrOpen3PaneWindow().then(win => + Services.ww.openWindow( + win, + "chrome://messenger/content/messageWindow.xhtml", + "_blank", + "all,chrome,dialog=no,status,toolbar", + uri + ) + ); + } +} -- cgit v1.2.3