//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.Mvc { using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Web; using System.Web.Mvc; using System.Web.Routing; using System.Web.UI; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId; using DotNetOpenAuth.OpenId.RelyingParty; using Validation; /// /// Methods that generate HTML or Javascript for hosting AJAX OpenID "controls" on /// ASP.NET MVC web sites. /// public static class OpenIdHelper { /// /// Emits a series of stylesheet import tags to support the AJAX OpenID Selector. /// /// The on the view. /// HTML that should be sent directly to the browser. [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "False positive.")] public static string OpenIdSelectorStyles(this HtmlHelper html) { Requires.NotNull(html, "html"); using (var result = new StringWriter(CultureInfo.CurrentCulture)) { result.WriteStylesheetLink(OpenId.RelyingParty.OpenIdSelector.EmbeddedStylesheetResourceName); result.WriteStylesheetLink(OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedStylesheetResourceName); return result.ToString(); } } /// /// Emits a series of script import tags and some inline script to support the AJAX OpenID Selector. /// /// The on the view. /// HTML that should be sent directly to the browser. public static string OpenIdSelectorScripts(this HtmlHelper html) { return OpenIdSelectorScripts(html, null, null); } /// /// Emits a series of script import tags and some inline script to support the AJAX OpenID Selector. /// /// The on the view. /// An optional instance of an control, whose properties have been customized to express how this MVC control should be rendered. /// An optional set of additional script customizations. /// /// HTML that should be sent directly to the browser. /// [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "False positive")] public static string OpenIdSelectorScripts(this HtmlHelper html, OpenIdSelector selectorOptions, OpenIdAjaxOptions additionalOptions) { Requires.NotNull(html, "html"); bool selectorOptionsOwned = false; if (selectorOptions == null) { selectorOptionsOwned = true; selectorOptions = new OpenId.RelyingParty.OpenIdSelector(); } try { if (additionalOptions == null) { additionalOptions = new OpenIdAjaxOptions(); } using (StringWriter result = new StringWriter(CultureInfo.CurrentCulture)) { if (additionalOptions.ShowDiagnosticIFrame || additionalOptions.ShowDiagnosticTrace) { string scriptFormat = @"window.openid_visible_iframe = {0}; // causes the hidden iframe to show up window.openid_trace = {1}; // causes lots of messages"; result.WriteScriptBlock(string.Format( CultureInfo.InvariantCulture, scriptFormat, additionalOptions.ShowDiagnosticIFrame ? "true" : "false", additionalOptions.ShowDiagnosticTrace ? "true" : "false")); } var scriptResources = new[] { OpenIdRelyingPartyControlBase.EmbeddedJavascriptResource, OpenIdRelyingPartyAjaxControlBase.EmbeddedAjaxJavascriptResource, OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedScriptResourceName, }; result.WriteScriptTags(scriptResources); if (selectorOptions.DownloadYahooUILibrary) { result.WriteScriptTagsUrls(new[] { "https://ajax.googleapis.com/ajax/libs/yui/2.8.0r4/build/yuiloader/yuiloader-min.js" }); } using (var blockBuilder = new StringWriter(CultureInfo.CurrentCulture)) { if (selectorOptions.DownloadYahooUILibrary) { blockBuilder.WriteLine(@" try { if (YAHOO) { var loader = new YAHOO.util.YUILoader({ require: ['button', 'menu'], loadOptional: false, combine: true }); loader.insert(); } } catch (e) { }"); } blockBuilder.WriteLine("window.aspnetapppath = '{0}';", VirtualPathUtility.AppendTrailingSlash(HttpContext.Current.Request.ApplicationPath)); // Positive assertions can last no longer than this library is willing to consider them valid, // and when they come with OP private associations they last no longer than the OP is willing // to consider them valid. We assume the OP will hold them valid for at least five minutes. double assertionLifetimeInMilliseconds = Math.Min(TimeSpan.FromMinutes(5).TotalMilliseconds, Math.Min(OpenIdElement.Configuration.MaxAuthenticationTime.TotalMilliseconds, DotNetOpenAuthSection.Messaging.MaximumMessageLifetime.TotalMilliseconds)); blockBuilder.WriteLine( "{0} = {1};", OpenIdRelyingPartyAjaxControlBase.MaxPositiveAssertionLifetimeJsName, assertionLifetimeInMilliseconds.ToString(CultureInfo.InvariantCulture)); if (additionalOptions.PreloadedDiscoveryResults != null) { blockBuilder.WriteLine(additionalOptions.PreloadedDiscoveryResults); } string discoverUrl = VirtualPathUtility.AppendTrailingSlash(HttpContext.Current.Request.ApplicationPath) + html.RouteCollection["OpenIdDiscover"].GetVirtualPath(html.ViewContext.RequestContext, new RouteValueDictionary(new { identifier = "xxx" })).VirtualPath; string blockFormat = @" {0} = function (argument, resultFunction, errorCallback) {{ jQuery.ajax({{ async: true, dataType: 'text', error: function (request, status, error) {{ errorCallback(status, argument); }}, success: function (result) {{ resultFunction(result, argument); }}, url: '{1}'.replace('xxx', encodeURIComponent(argument)) }}); }};"; blockBuilder.WriteLine(blockFormat, OpenIdRelyingPartyAjaxControlBase.CallbackJSFunctionAsync, discoverUrl); blockFormat = @" window.postLoginAssertion = function (positiveAssertion) {{ $('#{0}')[0].setAttribute('value', positiveAssertion); if ($('#{1}')[0] && !$('#{1}')[0].value) {{ // popups have no ReturnUrl predefined, but full page LogOn does. $('#{1}')[0].setAttribute('value', window.parent.location.href); }} document.forms[{2}].submit(); }};"; blockBuilder.WriteLine( blockFormat, additionalOptions.AssertionHiddenFieldId, additionalOptions.ReturnUrlHiddenFieldId, additionalOptions.FormKey); blockFormat = @" $(function () {{ var box = document.getElementsByName('openid_identifier')[0]; initAjaxOpenId(box, {0}, {1}, {2}, {3}, {4}, {5}, null, // js function to invoke on receiving a positive assertion {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17}, false, // auto postback null); // PostBackEventReference (unused in MVC) }});"; blockBuilder.WriteLine( blockFormat, MessagingUtilities.GetSafeJavascriptValue(Util.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenIdTextBox.EmbeddedLogoResourceName)), MessagingUtilities.GetSafeJavascriptValue(Util.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedSpinnerResourceName)), MessagingUtilities.GetSafeJavascriptValue(Util.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName)), MessagingUtilities.GetSafeJavascriptValue(Util.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginFailureResourceName)), selectorOptions.Throttle, selectorOptions.Timeout.TotalMilliseconds, MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnText), MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnToolTip), selectorOptions.TextBox.ShowLogOnPostBackButton ? "true" : "false", MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnPostBackToolTip), MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.RetryText), MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.RetryToolTip), MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.BusyToolTip), MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.IdentifierRequiredMessage), MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnInProgressMessage), MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.AuthenticationSucceededToolTip), MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.AuthenticatedAsToolTip), MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.AuthenticationFailedToolTip)); result.WriteScriptBlock(blockBuilder.ToString()); result.WriteScriptTags(OpenId.RelyingParty.OpenIdSelector.EmbeddedScriptResourceName); Reporting.RecordFeatureUse("MVC " + typeof(OpenIdSelector).Name); return result.ToString(); } } } catch { if (selectorOptionsOwned) { selectorOptions.Dispose(); } throw; } } /// /// Emits the HTML to render an OpenID Provider button as a part of the overall OpenID Selector UI. /// /// The on the view. /// The OP Identifier. /// The URL of the image to display on the button. /// /// HTML that should be sent directly to the browser. /// public static string OpenIdSelectorOPButton(this HtmlHelper html, Identifier providerIdentifier, string imageUrl) { Requires.NotNull(html, "html"); Requires.NotNull(providerIdentifier, "providerIdentifier"); Requires.NotNullOrEmpty(imageUrl, "imageUrl"); return OpenIdSelectorButton(html, providerIdentifier, "OPButton", imageUrl); } /// /// Emits the HTML to render a generic OpenID button as a part of the overall OpenID Selector UI, /// allowing the user to enter their own OpenID. /// /// The on the view. /// The URL of the image to display on the button. /// /// HTML that should be sent directly to the browser. /// public static string OpenIdSelectorOpenIdButton(this HtmlHelper html, string imageUrl) { Requires.NotNull(html, "html"); Requires.NotNullOrEmpty(imageUrl, "imageUrl"); return OpenIdSelectorButton(html, "OpenIDButton", "OpenIDButton", imageUrl); } /// /// Emits the HTML to render the entire OpenID Selector UI. /// /// The on the view. /// The buttons to include on the selector. /// /// HTML that should be sent directly to the browser. /// [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "False positive.")] [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Not a problem for this type.")] public static string OpenIdSelector(this HtmlHelper html, params SelectorButton[] buttons) { Requires.NotNull(html, "html"); Requires.NotNull(buttons, "buttons"); using (var writer = new StringWriter(CultureInfo.CurrentCulture)) { using (var h = new HtmlTextWriter(writer)) { h.AddAttribute(HtmlTextWriterAttribute.Class, "OpenIdProviders"); h.RenderBeginTag(HtmlTextWriterTag.Ul); foreach (SelectorButton button in buttons) { var op = button as SelectorProviderButton; if (op != null) { h.Write(OpenIdSelectorOPButton(html, op.OPIdentifier, op.Image)); continue; } var openid = button as SelectorOpenIdButton; if (openid != null) { h.Write(OpenIdSelectorOpenIdButton(html, openid.Image)); continue; } ErrorUtilities.VerifySupported(false, "The {0} button is not yet supported for MVC.", button.GetType().Name); } h.RenderEndTag(); // ul if (buttons.OfType().Any()) { h.Write(OpenIdAjaxTextBox(html)); } } return writer.ToString(); } } /// /// Emits the HTML to render the control as a part of the overall /// OpenID Selector UI. /// /// The on the view. /// /// HTML that should be sent directly to the browser. /// [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "html", Justification = "Breaking change, and it's an extension method so it's useful.")] public static string OpenIdAjaxTextBox(this HtmlHelper html) { return @""; } /// /// Emits the HTML to render a button as a part of the overall OpenID Selector UI. /// /// The on the view. /// The value to assign to the HTML id attribute. /// The value to assign to the HTML class attribute. /// The URL of the image to draw on the button. /// /// HTML that should be sent directly to the browser. /// [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Not a problem for this type.")] [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "False positive.")] private static string OpenIdSelectorButton(this HtmlHelper html, string id, string cssClass, string imageUrl) { Requires.NotNull(html, "html"); Requires.NotNull(id, "id"); Requires.NotNullOrEmpty(imageUrl, "imageUrl"); using (var writer = new StringWriter(CultureInfo.CurrentCulture)) { using (var h = new HtmlTextWriter(writer)) { h.AddAttribute(HtmlTextWriterAttribute.Id, id); if (!string.IsNullOrEmpty(cssClass)) { h.AddAttribute(HtmlTextWriterAttribute.Class, cssClass); } h.RenderBeginTag(HtmlTextWriterTag.Li); h.AddAttribute(HtmlTextWriterAttribute.Href, "#"); h.RenderBeginTag(HtmlTextWriterTag.A); h.RenderBeginTag(HtmlTextWriterTag.Div); h.RenderBeginTag(HtmlTextWriterTag.Div); h.AddAttribute(HtmlTextWriterAttribute.Src, imageUrl); h.RenderBeginTag(HtmlTextWriterTag.Img); h.RenderEndTag(); h.AddAttribute(HtmlTextWriterAttribute.Src, Util.GetWebResourceUrl(typeof(OpenIdSelector), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName)); h.AddAttribute(HtmlTextWriterAttribute.Class, "loginSuccess"); h.AddAttribute(HtmlTextWriterAttribute.Title, "Authenticated as {0}"); h.RenderBeginTag(HtmlTextWriterTag.Img); h.RenderEndTag(); h.RenderEndTag(); // div h.AddAttribute(HtmlTextWriterAttribute.Class, "ui-widget-overlay"); h.RenderBeginTag(HtmlTextWriterTag.Div); h.RenderEndTag(); // div h.RenderEndTag(); // div h.RenderEndTag(); // a h.RenderEndTag(); // li } return writer.ToString(); } } /// /// Emits <script> tags that import a given set of scripts given their URLs. /// /// The writer to emit the tags to. /// The locations of the scripts to import. private static void WriteScriptTagsUrls(this TextWriter writer, IEnumerable scriptUrls) { Requires.NotNull(writer, "writer"); Requires.NotNull(scriptUrls, "scriptUrls"); foreach (string script in scriptUrls) { writer.WriteLine("", script); } } /// /// Writes out script tags that import a script from resources embedded in this assembly. /// /// The writer to emit the tags to. /// Name of the resource. private static void WriteScriptTags(this TextWriter writer, string resourceName) { Requires.NotNull(writer, "writer"); Requires.NotNullOrEmpty(resourceName, "resourceName"); WriteScriptTags(writer, new[] { resourceName }); } /// /// Writes out script tags that import scripts from resources embedded in this assembly. /// /// The writer to emit the tags to. /// The resource names. private static void WriteScriptTags(this TextWriter writer, IEnumerable resourceNames) { Requires.NotNull(writer, "writer"); Requires.NotNull(resourceNames, "resourceNames"); writer.WriteScriptTagsUrls(resourceNames.Select(r => Util.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), r))); } /// /// Writes a given script block, surrounding it with <script> and CDATA tags. /// /// The writer to emit the tags to. /// The script to inline on the page. private static void WriteScriptBlock(this TextWriter writer, string script) { Requires.NotNull(writer, "writer"); Requires.NotNullOrEmpty(script, "script"); writer.WriteLine(""); } /// /// Writes a given CSS link. /// /// The writer to emit the tags to. /// Name of the resource containing the CSS content. private static void WriteStylesheetLink(this TextWriter writer, string resourceName) { Requires.NotNull(writer, "writer"); Requires.NotNullOrEmpty(resourceName, "resourceName"); WriteStylesheetLinkUrl(writer, Util.GetWebResourceUrl(typeof(OpenIdRelyingPartyAjaxControlBase), resourceName)); } /// /// Writes a given CSS link. /// /// The writer to emit the tags to. /// The stylesheet to link in. private static void WriteStylesheetLinkUrl(this TextWriter writer, string stylesheet) { Requires.NotNull(writer, "writer"); Requires.NotNullOrEmpty(stylesheet, "stylesheet"); writer.WriteLine("", stylesheet); } } }