/* Copyright (c) Citrix Systems, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, * with or without modification, are permitted provided * that the following conditions are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Resources; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Windows.Forms; using System.Xml; using XenAdmin.Core; using XenAdmin.Network; using XenAPI; namespace XenAdmin.Plugins { /// /// Class describing a plugin feature which loads a URL to display as an extra tab inside XenCenter; this can be used /// to allow access to web management consoles or to add additional user interface features into XenCenter. /// internal class TabPageFeature : Feature { /* * Each TabPageFeature has an associated WebBrowser2 instance. There is just one per feature, and therefore they are shared if a * feature appears on multiple XenObjects. Each time the user switches XenObject, the state of the browser for that particular * XenObject is persisted in BrowserStates. * * * We have a fairly complicated scheme for managing credentials for webpages: * * Normally, when you hit a page that requires authorization, IE will pop up a prompt. We intercept this prompt, and instead we * persist credentials on the XenServer host. We also prompt if we see HTTP response 401 Authorization Required or 403 Forbidden, * on the assumption that the password on the server has changed. * * Credentials are persisted server-side as Secrets, with a reference to the appropriate Secret stored in Pool.gui_config (see * Pool.*XCPluginSecret). If the user chooses not to persist credentials, we still persist an empty string in Pool.gui_config, * to record the fact that the user has made that choice. * * When we see an authentication request, we enter Browser_AuthenticationPrompt on the UI thread. We check the server for * persisted credentials, which has to occur on a background thread, but we can't return from Browser_AuthenticationPrompt until * we have the credentials. To handle this, we spawn a thread in TriggerGetSecret, and then use Application.DoEvents to wait on * the UI thread without blocking redraws. See CompleteGetSecret. The background thread will set BrowserState.Credentials when * it is finished, releasing the UI thread. * * If there's nothing in Pool.gui_config for us, or if there is the empty string (indicating that the user has chosen not to * persist credentials), then we need to prompt for the credentials from the user. In that case, it's back onto the UI thread * to show a TabPageCredentialsDialog before returning to the background thread to persist the new credentials. */ [DllImport("wininet.dll", SetLastError = true)] private static extern long DeleteUrlCacheEntry(string url); #region Fields private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); private const char CREDENTIALS_SEPARATOR = '\x0294'; /// /// required - "url" attribute, the local or remote url of the HTML page to load /// private readonly string Url; /// /// optional - "context-menu" attribute, if set the context menu will be disabled /// private readonly bool ContextEnabled; /// /// optional - "xencenter-only" attribute, if set this tabpage will appear on the XenCenter node and no where else /// private readonly bool XenCenterOnly; /// /// optional - "relative" attribute, if set the url will be resolved relative to the XenCenter directory /// private readonly bool RelativeUrl; /// /// optional - "help-link" attribute, if set when the help button is pressed this url will be loaded in a separate browser /// private readonly string HelpLink; /// /// optional - "credentials" attribute, if set XenCenter will duplicate a session for use by the htmls JavaScript /// private readonly bool Credentials; /// /// optional - "console" attribute on the "TabPage" tag. /// Indicates that this tab-page is a replacement for the console tab. /// private readonly bool Console; public const string ELEMENT_NAME = "TabPage"; public const string ATT_XC_ONLY = "xencenter-only"; public const string ATT_CONTEXT_MENU = "context-menu"; public const string ATT_HELP_LINK = "help-link"; public const string ATT_RELATIVE = "relative"; public const string ATT_CREDENTIALS = "credentials"; public const string ATT_CONSOLE = "console"; public const string ATT_URL = "url"; private readonly Dictionary BrowserStates = new Dictionary(); private BrowserState lastBrowserState; private StatusStrip statusStrip; private ToolStripStatusLabel statusLabel; private TabPage _tabPage; private WebBrowser2 Browser; #endregion public TabPageFeature(ResourceManager resourceManager, XmlNode node, PluginDescriptor plugin) : base(resourceManager, node, plugin) { XenCenterOnly = Helpers.GetBoolXmlAttribute(node, ATT_XC_ONLY); ContextEnabled = Helpers.GetBoolXmlAttribute(node, ATT_CONTEXT_MENU); // plugins v2 HelpLink = Helpers.GetStringXmlAttribute(node, ATT_HELP_LINK, ""); RelativeUrl = Helpers.GetBoolXmlAttribute(node, ATT_RELATIVE); Credentials = Helpers.GetBoolXmlAttribute(node, ATT_CREDENTIALS); Console = Helpers.GetBoolXmlAttribute(node, ATT_CONSOLE); string urlString = Helpers.GetStringXmlAttribute(node, ATT_URL); Url = urlString == null ? "" : string.Format("{0}{1}", RelativeUrl ? string.Format("{0}/", Application.StartupPath) : "", urlString); } #region Properties public bool HasHelp { get { return !string.IsNullOrEmpty(HelpLink); } } public TabPage TabPage { get { return _tabPage; } } public IXenObject SelectedXenObject { get; set; } /// /// Gets a value indicating whether this instance should replace the console tab. /// public bool IsConsoleReplacement { get { return Console; } } /// /// Gets a value indicating whether the most recent navigation for the selected xen object /// resulted in an error. /// public bool IsError { get { if (SelectedXenObject != null && BrowserStates.ContainsKey(SelectedXenObject)) return BrowserStates[SelectedXenObject].IsError; return false; } } #endregion public override string CheckForError() { if (Url == "") return string.Format(Messages.CANNOT_PARSE_NODE_PARAM, node.Name, ATT_URL); return base.CheckForError(); } public override void Initialize() { _tabPage = new TabPage(); try { _tabPage.SuspendLayout(); _tabPage.Text = ToString(); _tabPage.ToolTipText = Tooltip ?? ""; if (Program.MainWindow != null) // for unit tests _tabPage.HelpRequested += Program.MainWindow.MainWindow_HelpRequested; CreateStatusBar(); CreateBrowser(); _tabPage.Controls.Add(statusStrip); _tabPage.Controls.Add(Browser); Browser.BringToFront(); _tabPage.Tag = this; // Tag the tab page so we can reload when the tab is focused. } finally { _tabPage.ResumeLayout(); } } private void CreateStatusBar() { statusLabel = new ToolStripStatusLabel { Overflow = ToolStripItemOverflow.Never, Spring = true, TextAlign = System.Drawing.ContentAlignment.MiddleLeft, }; statusStrip = new StatusStrip { SizingGrip = false, AutoSize = false, Items = {statusLabel}, Visible = false }; } private void CreateBrowser() { Browser = new WebBrowser2 {Dock = DockStyle.Fill, IsWebBrowserContextMenuEnabled = ContextEnabled}; Browser.ProgressChanged += Browser_ProgressChanged; Browser.DocumentCompleted += Browser_DocumentCompleted; Browser.Navigating += Browser_Navigating; Browser.Navigated += Browser_Navigated; Browser.NavigateError += Browser_NavigateError; Browser.WindowClosed += Browser_WindowClosed; Browser.PreviewKeyDown += Browser_PreviewKeyDown; Browser.AuthenticationPrompt += Browser_AuthenticationPrompt; // Navigate to about:blank to work around http://support.microsoft.com/kb/320153. Browser.Navigate("about:blank"); } public bool ShowTab { get { try { if (_tabPage == null) return false; if (XenCenterOnly) return SelectedXenObject == null; if (SelectedXenObject == null || !Enabled || !Placeholders.UriValid(Url, SelectedXenObject)) return false; return Search == null || Search.Query.Match(SelectedXenObject); } catch (UriFormatException e) { log.Debug(string.Format("Cannot display tab '{0}' for plugin '{1}'. Invalid properties in url.", ToString(), PluginDescriptor.Name), e); return false; } } } public void SetUrl() { // Never update XenCenter node tabs once loaded if (Browser.Url != null && Browser.Url.AbsoluteUri != "about:blank" && XenCenterOnly) return; BrowserState state; if (SelectedXenObject == null) { // XenCenter node is selected, the placeholder code will sub in "null" for all placeholders // After this point we will never update this url again for this node, so there is no need to store a browser state state = new BrowserState(Placeholders.SubstituteUri(Url, SelectedXenObject), SelectedXenObject, Browser); } else if (BrowserStates.ContainsKey(SelectedXenObject)) { state = BrowserStates[SelectedXenObject]; state.Uris = Placeholders.SubstituteUri(Url, SelectedXenObject); } else { state = new BrowserState(Placeholders.SubstituteUri(Url, SelectedXenObject), SelectedXenObject, Browser); BrowserStates[SelectedXenObject] = state; } try { if (state.ObjectForScripting != null) { if (Credentials) state.ObjectForScripting.LoginSession(); Browser.ObjectForScripting = state.ObjectForScripting; } lastBrowserState = state; Browser.DocumentText = string.Empty; Application.DoEvents(); lastBrowserState.IsError = false; ShowStatus(string.Format(Messages.WEB_BROWSER_WAITING, ShortUri(state.Uris[0]))); Browser.Navigate(state.Uris[0]); } catch (Exception e) { log.Error(string.Format("Failed to set TabPage url for plugin '{0}'", PluginDescriptor.Name), e); } } private void ShowStatus(string text) { statusLabel.Text = text; statusStrip.Visible = true; } private void HideStatus() { statusStrip.Visible = false; } private string ShortUri(Uri uri) { return uri.ToString().Ellipsise(80); } #region WebBrowser2 event handlers private void Browser_Navigating(object sender, WebBrowserNavigatingEventArgs e) { Program.AssertOnEventThread(); if (XenCenterOnly || lastBrowserState == null) return; log.DebugFormat("Navigating to last browser state url for {0}", Helpers.GetName(lastBrowserState.Obj)); DeleteUrlCacheEntry(e.Url.AbsoluteUri); if (Console) { // delete this page from the cache.... that we can be sure that if the page stops working then // the user reliably gets the real console back. DeleteUrlCacheEntry(e.Url.AbsoluteUri); } } private void Browser_NavigateError(object sender, WebBrowser2.NavigateErrorEventArgs e) { Program.AssertOnEventThread(); lastBrowserState.IsError = true; log.DebugFormat("Navigation failed with error {0}.", e.StatusCode); try { if (e.StatusCode == 401 || e.StatusCode == 403) { log.Debug("Clearing secret and re-prompting."); CompleteClearSecret(lastBrowserState); TriggerGetSecret(lastBrowserState); } } catch (Exception exn) { log.Error(exn, exn); } } private void Browser_Navigated(object sender, WebBrowserNavigatedEventArgs e) { if (XenCenterOnly || lastBrowserState == null) { HideStatus(); return; } log.DebugFormat("Navigated to last browser state url for {0}", Helpers.GetName(lastBrowserState.Obj)); if (lastBrowserState.IsError && e.Url != null && e.Url.AbsoluteUri != "about:blank") { if (Console) { // if this plugin tab-page is a console replacement and the error-state of the navigation has changed // then update the tabs. This ensures that user gets the real console tab back. Program.MainWindow.UpdateToolbars(); } lastBrowserState.Uris.Remove(e.Url); if (lastBrowserState.Uris.Count > 0) { lastBrowserState.IsError = false; ShowStatus(string.Format(Messages.WEB_BROWSER_FAILED_RETRYING, ShortUri(e.Url), ShortUri(lastBrowserState.Uris[0]))); Browser.Navigate(lastBrowserState.Uris[0]); } else { ShowStatus(string.Format(Messages.WEB_BROWSER_FAILED, ShortUri(e.Url))); Browser.Navigate("about:blank"); } } } private void Browser_ProgressChanged(object sender, WebBrowserProgressChangedEventArgs e) { Program.AssertOnEventThread(); if (XenCenterOnly || lastBrowserState == null || lastBrowserState.Uris.Count == 0 || Browser.ReadyState == WebBrowserReadyState.Complete) return; if (e.MaximumProgress > 0 && e.CurrentProgress >= 0 && e.CurrentProgress < e.MaximumProgress) { int progr = Convert.ToInt32(e.CurrentProgress * 100L / e.MaximumProgress); if (progr > 100) progr = 100; ShowStatus(string.Format(Messages.WEB_BROWSER_LOADING_PERCENT, ShortUri(lastBrowserState.Uris[0]), progr)); } else { ShowStatus(string.Format(Messages.WEB_BROWSER_LOADING, ShortUri(lastBrowserState.Uris[0]))); } } private void Browser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e) { Program.AssertOnEventThread(); if (XenCenterOnly || lastBrowserState == null || lastBrowserState.IsError) return; HideStatus(); } private void Browser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) { if (e.KeyCode == Keys.F1 && Browser.Focused) { Program.MainWindow.MainWindow_HelpRequested(null, null); } } private void Browser_AuthenticationPrompt(WebBrowser2 sender, WebBrowser2.AuthenticationPromptEventArgs e) { try { Program.AssertOnEventThread(); log.Debug("Prompting for authentication..."); CompleteClearSecret(lastBrowserState); CompleteGetSecret(lastBrowserState); BrowserState.BrowserCredentials creds = lastBrowserState.Credentials; if (creds != null && creds.Valid) { e.Username = creds.Username; e.Password = creds.Password; e.Success = true; log.Debug("Prompt for authentication successful."); } else { e.Success = false; log.Debug("Prompt for authentication cancelled / failed."); } } catch (Exception exn) { log.Error("Prompt for authentication failed", exn); e.Success = false; } } private void Browser_WindowClosed(object sender, EventArgs e) { if (!Program.Exiting && Enabled && !_tabPage.Disposing) { _tabPage.Controls.Remove(Browser); CreateBrowser(); _tabPage.Controls.Add(Browser); if (SelectedXenObject != null) { BrowserStates.Remove(SelectedXenObject); SetUrl(); } } } #endregion private void CompleteGetSecret(BrowserState state) { if (state.Credentials != null) return; TriggerGetSecret(state); while (state.Credentials == null) Application.DoEvents(); } /// /// Load the secret where we've saved credentials. This will be placed on a background thread, and we'll poll for /// its results. /// private void TriggerGetSecret(BrowserState state) { Thread t = new Thread(GetSecret); t.IsBackground = true; t.Start(state); } /// /// Get the persisted secret from the server, or prompt for new credentials and persist those back to the server. /// Completion of this thread is indicated by state.Credentials being set. /// /// private void GetSecret(object obj) { Program.AssertOffEventThread(); BrowserState state = (BrowserState)obj; try { Session session = state.Obj.Connection.Session; do { Pool pool = Helpers.GetPoolOfOne(state.Obj.Connection); if (pool == null) { log.Warn("Failed to get Pool!"); Thread.Sleep(5000); continue; } string secret_uuid = pool.GetXCPluginSecret(PluginDescriptor.Name, state.Obj); if (string.IsNullOrEmpty(secret_uuid)) { var msg = secret_uuid == null ? "Nothing persisted." : "User chose not to persist these credentials."; log.Debug(msg + " Prompting for new credentials."); Program.Invoke(Program.MainWindow, () => { state.Credentials = PromptForUsernamePassword(secret_uuid == null); }); MaybePersistCredentials(session, pool, state.Obj, state.Credentials); return; } else { log.Debug("Found a secret."); XenRef secret = null; try { secret = Secret.get_by_uuid(session, secret_uuid); } catch (Failure exn) { log.Warn(string.Format("Secret with uuid {0} for {1} on plugin {2} has disappeared! Removing from pool.gui_config.", secret_uuid, Helpers.GetName(state.Obj), PluginDescriptor.Name), exn); TryToRemoveSecret(pool, session, PluginDescriptor.Name, state.Obj); continue; } string val = Secret.get_value(session, secret); string[] bits = val.Split(CREDENTIALS_SEPARATOR); if (bits.Length != 2) { log.WarnFormat("Corrupt secret with uuid {0} for {1} on plugin {2}! Deleting.", secret_uuid, Helpers.GetName(state.Obj), PluginDescriptor.Name); TryToDestroySecret(session, secret.opaque_ref); TryToRemoveSecret(pool, session, PluginDescriptor.Name, state.Obj); continue; } log.Debug("Secret successfully read."); state.Credentials = new BrowserState.BrowserCredentials { Username = bits[0], Password = bits[1], PersistCredentials = true, Valid = true }; return; } // Unreachable. Should either have returned, or continued (to retry). } while (true); } catch (Exception exn) { log.Warn("Ignoring exception when trying to get secret", exn); // Note that it's essential that we set state.Credentials before leaving this function, // because other threads are waiting for that value to appear. state.Credentials = new BrowserState.BrowserCredentials {Valid = false}; } } private void MaybePersistCredentials(Session session, Pool pool, IXenObject obj, BrowserState.BrowserCredentials creds) { if (creds != null && creds.Valid) { string secretUuid = creds.PersistCredentials ? CreateSecret(session, creds.Username, creds.Password) : ""; pool.SetXCPluginSecret(session, PluginDescriptor.Name, obj, secretUuid); } } private string CreateSecret(Session session, string username, string password) { string val = string.Format("{0}{1}{2}", username, CREDENTIALS_SEPARATOR, password); return Secret.CreateSecret(session, val); } private BrowserState.BrowserCredentials PromptForUsernamePassword(bool persistCredentials) { Program.AssertOnEventThread(); using (var d = new TabPageCredentialsDialog {ServiceName = Label, PersistCredentials = persistCredentials}) { if (d.ShowDialog(Program.MainWindow) == DialogResult.OK) { return new BrowserState.BrowserCredentials { Username = d.Username, Password = d.Password, PersistCredentials = d.PersistCredentials, Valid = true }; } return new BrowserState.BrowserCredentials {Valid = false}; } } private void CompleteClearSecret(BrowserState state) { if (state.Credentials == null) return; TriggerClearSecret(state); while (state.Credentials != null) Application.DoEvents(); } private void TriggerClearSecret(BrowserState state) { Thread t = new Thread(ClearSecret); t.IsBackground = true; t.Start(state); } /// /// Clear the persisted secret from the server. /// Completion of this thread is indicated by state.Credentials being set to null. /// private void ClearSecret(object obj) { Program.AssertOffEventThread(); BrowserState state = (BrowserState)obj; try { Session session = state.Obj.Connection.Session; Pool pool = Helpers.GetPoolOfOne(state.Obj.Connection); if (pool == null) return; string secret_uuid = pool.GetXCPluginSecret(PluginDescriptor.Name, state.Obj); if (!string.IsNullOrEmpty(secret_uuid)) { XenRef secret_ref = Secret.get_by_uuid(session, secret_uuid); TryToDestroySecret(session, secret_ref.opaque_ref); TryToRemoveSecret(pool, session, PluginDescriptor.Name, state.Obj); } } catch (Exception exn) { log.Warn("Ignoring exception when trying to clear secret", exn); } state.Credentials = null; } private static void TryToDestroySecret(Session session, string opaque_ref) { try { Secret.destroy(session, opaque_ref); log.DebugFormat("Successfully removed secret with opaque_ref {0}", opaque_ref); } catch (Exception exn) { log.Error(string.Format("Failed to destroy secret with opaque_ref {0}", opaque_ref), exn); } } private static void TryToRemoveSecret(Pool pool, Session session, string name, IXenObject obj) { try { pool.RemoveXCPluginSecret(session, name, obj); log.DebugFormat("Successfully removed secret for plugin {0} on object {1}.", name, Helpers.GetName(obj)); } catch (Exception exn) { log.Error(string.Format("Failed to remove secret for plugin {0} on object {1}.", name, Helpers.GetName(obj)), exn); } } protected override void Dispose(bool disposing) { base.Dispose(disposing); BrowserStates.Clear(); if (Browser != null) { Browser.Dispose(); Browser = null; } if (_tabPage != null) { _tabPage.Dispose(); _tabPage = null; } } public void DisposeURL(IXenObject xmo) { if (BrowserStates.ContainsKey(xmo)) { BrowserStates.Remove(xmo); } } public void LaunchHelp() { if (HasHelp) { Program.OpenURL(HelpLink); } } private class BrowserState { /// /// May be null, if this is the XenCenter node. /// public readonly ScriptingObject ObjectForScripting; public List Uris; /// /// May be null, if this is the XenCenter node. /// public IXenObject Obj; /// /// May be null. /// public BrowserCredentials Credentials = null; /// /// Indicates that the browser is currently in an error state i.e. the page couldn't be loaded. This is used when the console is being replaced /// by this tab-page so that the user can get the real console back. /// public bool IsError; public BrowserState(List uris, IXenObject obj, WebBrowser2 browser) { Uris = new List(uris); Obj = obj; ObjectForScripting = obj == null ? null : new ScriptingObject(browser, obj); } public class BrowserCredentials { /// /// True if the other fields here are valid (i.e. the user pressed OK, not Cancel, when prompted for credentials). /// internal bool Valid; internal string Username; internal string Password; internal bool PersistCredentials; } } } [ComVisible(true)] public class ScriptingObject { private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); public const string JAVASCRIPT_CALLBACK_METHOD = "XenCenterXML"; [Obsolete("Use SessionOpaqueRef instead.")] public string SessionUuid; public string SessionOpaqueRef; public string SessionUrl; private IXenConnection connection; public WebBrowser2 browser; public string SelectedObjectType; public string SelectedObjectRef; public ScriptingObject(WebBrowser2 Browser, IXenObject XenObject) { browser = Browser; connection = XenObject.Connection; if (connection != null) { connection.ConnectionResult += connection_ConnectionResult; } SetObject(XenObject); } public void SetObject(IXenObject XenObject) { // Annoyingly all of our classes start with an uppercase character, whereas the servers only uppercase abbreviations var abbreviations = new List {"SR", "VDI", "VBD", "VM", "PIF", "VIF", "PBD"}; SelectedObjectType = XenObject.GetType().Name; if (abbreviations.Find(s => SelectedObjectType.StartsWith(s)) == null) { string firstLetter = SelectedObjectType.Substring(0, 1); SelectedObjectType = firstLetter.ToLowerInvariant() + SelectedObjectType.Substring(1, SelectedObjectType.Length - 1); } SelectedObjectRef = XenObject.opaque_ref; } private void connection_ConnectionResult(object sender, ConnectionResultEventArgs e) { if (connection.IsConnected) { // If the connection has been reconnected then we need to update the session ref, as the old one is invalid LoginSession(); Program.Invoke(Program.MainWindow, delegate { if (!browser.IsDisposed && browser.Document != null) browser.Document.InvokeScript("RefreshPage"); else log.Debug("Tried to access disposed web browser, ignoring refresh."); }); } } public void LoginSession() { if (connection != null) { SessionUrl = connection.Session.Url; SessionOpaqueRef = connection.DuplicateSession().opaque_ref; #pragma warning disable 612, 618 SessionUuid = SessionOpaqueRef; #pragma warning restore 612, 618 } } /// /// This is the method called by any javascript logic in the webpage to make an api call. /// We take the data and post it straight through to xapi on a background thread to allow /// the calling method in the JS to exit and the page to continue drawing. /// /// Method name we will call in the JS to return the server response /// public void WriteXML(string jsCallback, string data) { Thread t = new Thread(writeXML); t.IsBackground = true; t.Start(new string[] { jsCallback, data }); } private void writeXML(object context) { try { // We post to the json url so that xapi returns the information in a way that is easy to consume by the JS string[] jsCallbackAndData = context as string[]; HttpWebRequest request = (HttpWebRequest)WebRequest.Create(string.Format("{0}/json", SessionUrl)); request.Method = "POST"; request.ContentType = "xml"; request.ContentLength = Encoding.UTF8.GetBytes(jsCallbackAndData[1]).Length; request.UserAgent = BrandManager.BRAND_CONSOLE + "\\Plugin"; request.Proxy = XenAdminConfigManager.Provider.GetProxyFromSettings(connection, true); using (var req = request.GetRequestStream()) using (StreamWriter xmlstream = new StreamWriter(req)) xmlstream.Write(jsCallbackAndData[1]); WebResponse response = request.GetResponse(); StringBuilder outputBuilder = new StringBuilder(); using (var res = response.GetResponseStream()) { if (res != null) using (StreamReader reader = new StreamReader(res)) while (reader.Peek() != -1) outputBuilder.Append(reader.ReadLine()); } // The xmlrpc bit of jquery expects a function object, which turns up here as a string that you could eval to get the function // We just want the name, so we filter it out. Regex functionNameReg = new Regex("^function(.*)\\("); string funcName = ""; Match m = functionNameReg.Match(jsCallbackAndData[0]); if (m.Success) { funcName = m.Groups[1].Value.Trim(); } Program.Invoke(Program.MainWindow, delegate { if (browser.IsDisposed || browser.Disposing) { log.Debug("Browser has been disposed, cannot return message to plugin."); } else if (browser.ObjectForScripting != this) { // If you don't do this, you can get old data re-entering the javascript execution after you have switched to a new object log.Debug("Scripting object has been changed, discarding message to plugin."); } else if (browser.Document != null) { browser.Document.InvokeScript(JAVASCRIPT_CALLBACK_METHOD, new object[] { funcName, outputBuilder.ToString() }); } }); } catch (Exception e) { log.Error(e); } //TODO: What's the sensible way to let the JS know that there has been an error? Invoke a method with the info? How does this work with regards to message callback? } } }