//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Web.UI.WebControls { using System.IO; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.UI; using System.Web.Caching; using System.Web; using System; using System.Collections; using System.Collections.Specialized; using System.ComponentModel; using System.ComponentModel.Design; using System.Drawing.Design; using System.Xml; using System.Globalization; using System.Web.Util; using System.Reflection; using System.Text; /// /// Displays a randomly selected ad banner on a page. /// [ DefaultEvent("AdCreated"), DefaultProperty("AdvertisementFile"), Designer("System.Web.UI.Design.WebControls.AdRotatorDesigner, " + AssemblyRef.SystemDesign), ToolboxData("<{0}:AdRotator runat=\"server\">") ] public class AdRotator : DataBoundControl { private static readonly object EventAdCreated = new object(); private const string XmlDocumentTag = "Advertisements"; private const string XmlDocumentRootXPath = "/" + XmlDocumentTag; private const string XmlAdTag = "Ad"; private const string KeywordProperty = "Keyword"; private const string ImpressionsProperty = "Impressions"; // static copy of the Random object. This is a pretty hefty object to // initialize, so you don't want to create one each time. private static Random _random; private String _baseUrl; private string _advertisementFile; private AdCreatedEventArgs _adCreatedEventArgs; private AdRec [] _adRecs; private bool _isPostCacheAdHelper; private string _uniqueID; private static readonly Type _adrotatorType = typeof(AdRotator); private static readonly Type[] _AdCreatedParameterTypes = {typeof(AdCreatedEventArgs)}; /// /// Initializes a new instance of the class. /// public AdRotator() { } /// /// Gets or sets the path to the XML file that contains advertisement data. /// [ Bindable(true), WebCategory("Behavior"), DefaultValue(""), Editor("System.Web.UI.Design.XmlUrlEditor, " + AssemblyRef.SystemDesign, typeof(UITypeEditor)), UrlProperty(), WebSysDescription(SR.AdRotator_AdvertisementFile) ] public string AdvertisementFile { get { return((_advertisementFile == null) ? String.Empty : _advertisementFile); } set { _advertisementFile = value; } } [ WebCategory("Behavior"), DefaultValue(AdCreatedEventArgs.AlternateTextElement), WebSysDescription(SR.AdRotator_AlternateTextField) ] public String AlternateTextField { get { String s = (String) ViewState["AlternateTextField"]; return((s != null) ? s : AdCreatedEventArgs.AlternateTextElement); } set { ViewState["AlternateTextField"] = value; } } /// /// The base url corresponds for mapping of other url elements such as /// imageUrl and navigateUrl. /// internal String BaseUrl { get { if (_baseUrl == null) { // Deal with app relative syntax (e.g. ~/foo) string tplSourceDir = TemplateControlVirtualDirectory.VirtualPathString; // For the AdRotator, use the AdvertisementFile directory as the base, and fall back to the // page/user control location as the base. String absoluteFile = null; String fileDirectory = null; if (!String.IsNullOrEmpty(AdvertisementFile)) { absoluteFile = UrlPath.Combine(tplSourceDir, AdvertisementFile); fileDirectory = UrlPath.GetDirectory(absoluteFile); } _baseUrl = string.Empty; if (fileDirectory != null) { _baseUrl = fileDirectory; } if (_baseUrl.Length == 0) { _baseUrl = tplSourceDir; } } return _baseUrl; } } /// /// /// Font property. Has no effect on this control, so hide it. /// [ Browsable(false), EditorBrowsableAttribute(EditorBrowsableState.Never), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), ] public override FontInfo Font { get { return base.Font; } } [ WebCategory("Behavior"), DefaultValue(AdCreatedEventArgs.ImageUrlElement), WebSysDescription(SR.AdRotator_ImageUrlField) ] public String ImageUrlField { get { String s = (String) ViewState["ImageUrlField"]; return((s != null) ? s : AdCreatedEventArgs.ImageUrlElement); } set { ViewState["ImageUrlField"] = value; } } private bool IsTargetSet { get { return (ViewState["Target"] != null); } } internal bool IsPostCacheAdHelper { get { return _isPostCacheAdHelper; } set { _isPostCacheAdHelper = value; } } /// /// Gets or sets a category keyword used for matching related advertisements in the advertisement file. /// [ Bindable(true), WebCategory("Behavior"), DefaultValue(""), WebSysDescription(SR.AdRotator_KeywordFilter) ] public string KeywordFilter { get { string s = (string)ViewState["KeywordFilter"]; return((s == null) ? String.Empty : s); } set { // trim the filter value if (String.IsNullOrEmpty(value)) { ViewState.Remove("KeywordFilter"); } else { ViewState["KeywordFilter"] = value.Trim(); } } } [ WebCategory("Behavior"), DefaultValue(AdCreatedEventArgs.NavigateUrlElement), WebSysDescription(SR.AdRotator_NavigateUrlField) ] public String NavigateUrlField { get { String s = (String) ViewState["NavigateUrlField"]; return((s != null) ? s : AdCreatedEventArgs.NavigateUrlElement); } set { ViewState["NavigateUrlField"] = value; } } private AdCreatedEventArgs SelectedAdArgs { get { return _adCreatedEventArgs; } set { _adCreatedEventArgs = value; } } /// /// Gets /// or sets the name of the browser window or frame to display the advertisement. /// [ Bindable(true), WebCategory("Behavior"), DefaultValue("_top"), WebSysDescription(SR.AdRotator_Target), TypeConverter(typeof(TargetConverter)) ] public string Target { get { string s = (string)ViewState["Target"]; return((s == null) ? "_top" : s); } set { ViewState["Target"] = value; } } protected override HtmlTextWriterTag TagKey { get { return HtmlTextWriterTag.A; } } public override string UniqueID { get { if (_uniqueID == null) { _uniqueID = base.UniqueID; } return _uniqueID; } } /// /// Occurs once per round trip after the creation of the /// control before the page is rendered. /// [ WebCategory("Action"), WebSysDescription(SR.AdRotator_OnAdCreated) ] public event AdCreatedEventHandler AdCreated { add { Events.AddHandler(EventAdCreated, value); } remove { Events.RemoveHandler(EventAdCreated, value); } } private void CheckOnlyOneDataSource() { int numOfDataSources = ((AdvertisementFile.Length > 0) ? 1 : 0); numOfDataSources += ((DataSourceID.Length > 0) ? 1 : 0); numOfDataSources += ((DataSource != null) ? 1 : 0); if (numOfDataSources > 1) { throw new HttpException(SR.GetString(SR.AdRotator_only_one_datasource, ID)); } } // Currently this is designed to be called when PostCache Substitution is being initialized internal void CopyFrom(AdRotator adRotator) { _adRecs = adRotator._adRecs; AccessKey = adRotator.AccessKey; AlternateTextField = adRotator.AlternateTextField; Enabled = adRotator.Enabled; ImageUrlField = adRotator.ImageUrlField; NavigateUrlField = adRotator.NavigateUrlField; TabIndex = adRotator.TabIndex; Target = adRotator.Target; ToolTip = adRotator.ToolTip; string id = adRotator.ID; if (!String.IsNullOrEmpty(id)) { ID = adRotator.ClientID; } // Below are properties that need to be handled specially and saved // to private variables. _uniqueID = adRotator.UniqueID; _baseUrl = adRotator.BaseUrl; // Special copy to properties that cannot be assigned directly if (adRotator.HasAttributes) { foreach(string key in adRotator.Attributes.Keys) { Attributes[key] = adRotator.Attributes[key]; } } if (adRotator.ControlStyleCreated) { ControlStyle.CopyFrom(adRotator.ControlStyle); } } private ArrayList CreateAutoGeneratedFields(IEnumerable dataSource) { if (dataSource == null) { return null; } ArrayList generatedFields = new ArrayList(); PropertyDescriptorCollection propertyDescriptors = null; if (dataSource is ITypedList) { propertyDescriptors = ((ITypedList)dataSource).GetItemProperties(new PropertyDescriptor[0]); } if (propertyDescriptors == null) { IEnumerator enumerator = dataSource.GetEnumerator(); if (enumerator.MoveNext()) { Object sampleItem = enumerator.Current; if (IsBindableType(sampleItem.GetType())) { // Raise error since we are expecting some record // containing multiple data values. throw new HttpException(SR.GetString(SR.AdRotator_expect_records_with_advertisement_properties, ID, sampleItem.GetType())); } else { propertyDescriptors = TypeDescriptor.GetProperties(sampleItem); } } } if (propertyDescriptors != null && propertyDescriptors.Count > 0) { foreach (PropertyDescriptor pd in propertyDescriptors) { if (IsBindableType(pd.PropertyType)) { generatedFields.Add(pd.Name); } } } return generatedFields; } // internal bool DoPostCacheSubstitutionAsNeeded(HtmlTextWriter writer) { if (!IsPostCacheAdHelper && SelectedAdArgs == null && Page.Response.HasCachePolicy && (int)Page.Response.Cache.GetCacheability() != (int)HttpCacheabilityLimits.None) { // The checking of the cacheability is to see if the page is output cached AdPostCacheSubstitution adPostCacheSubstitution = new AdPostCacheSubstitution(this); adPostCacheSubstitution.RegisterPostCacheCallBack(Context, Page, writer); return true; } return false; } /// /// Select an ad from ad records and create the event /// argument object. /// private AdCreatedEventArgs GetAdCreatedEventArgs() { IDictionary adInfo = SelectAdFromRecords(); AdCreatedEventArgs adArgs = new AdCreatedEventArgs(adInfo, ImageUrlField, NavigateUrlField, AlternateTextField); return adArgs; } private AdRec [] GetDataSourceData(IEnumerable dataSource) { ArrayList fields = CreateAutoGeneratedFields(dataSource); ArrayList adDicts = new ArrayList(); IEnumerator enumerator = dataSource.GetEnumerator(); while(enumerator.MoveNext()) { IDictionary dict = null; foreach (String field in fields){ if (dict == null) { dict = new HybridDictionary(); } dict.Add(field, DataBinder.GetPropertyValue(enumerator.Current, field)); } if (dict != null) { adDicts.Add(dict); } } return SetAdRecs(adDicts); } /// /// Gets the ad data for the given file by loading the file, or reading from the /// application-level cache. /// private AdRec [] GetFileData(string fileName) { // VSWhidbey 208626: Adopting similar code from xml.cs to support virtual path provider // First, figure out if it's a physical or virtual path VirtualPath virtualPath; string physicalPath; ResolvePhysicalOrVirtualPath(fileName, out virtualPath, out physicalPath); // try to get it from the ASP.NET cache string fileKey = CacheInternal.PrefixAdRotator + ((!String.IsNullOrEmpty(physicalPath)) ? physicalPath : virtualPath.VirtualPathString); CacheInternal cacheInternal = System.Web.HttpRuntime.CacheInternal; AdRec [] adRecs = cacheInternal[fileKey] as AdRec[]; if (adRecs == null) { // Otherwise load it CacheDependency dependency; try { using (Stream stream = OpenFileAndGetDependency(virtualPath, physicalPath, out dependency)) { adRecs = LoadStream(stream); Debug.Assert(adRecs != null); } } catch (Exception e) { if (!String.IsNullOrEmpty(physicalPath) && HttpRuntime.HasPathDiscoveryPermission(physicalPath)) { // We want to catch the error message, but not propage the inner exception. Otherwise we can throw up // logon prompts through IE; throw new HttpException(SR.GetString(SR.AdRotator_cant_open_file, ID, e.Message)); } else { throw new HttpException(SR.GetString(SR.AdRotator_cant_open_file_no_permission, ID)); } } // Cache it, but only if we got a dependency if (dependency != null) { using (dependency) { // and store it in the cache, dependent on the file name cacheInternal.UtcInsert(fileKey, adRecs, dependency); } } } return adRecs; } private static int GetRandomNumber(int maxValue) { if (_random == null) { _random = new Random(); } return _random.Next(maxValue) + 1; } private AdRec [] GetXmlDataSourceData(XmlDataSource xmlDataSource) { Debug.Assert(xmlDataSource != null); XmlDocument doc = xmlDataSource.GetXmlDocument(); if (doc == null) { return null; } return LoadXmlDocument(doc); } private bool IsBindableType(Type type) { return(type.IsPrimitive || (type == typeof(String)) || (type == typeof(DateTime)) || (type == typeof(Decimal))); } private bool IsOnAdCreatedOverridden() { bool result = false; Type type = this.GetType(); if (type != _adrotatorType) { MethodInfo methodInfo = type.GetMethod("OnAdCreated", BindingFlags.NonPublic | BindingFlags.Instance, null, _AdCreatedParameterTypes, null); if (methodInfo.DeclaringType != _adrotatorType) { result = true; } } return result; } private AdRec [] LoadFromXmlReader(XmlReader reader) { ArrayList adDicts = new ArrayList(); while (reader.Read()) { if (reader.Name == "Advertisements") { if (reader.Depth != 0) { return null; } break; } } while (reader.Read()) { if (reader.NodeType == XmlNodeType.Element && reader.Name == "Ad" && reader.Depth == 1) { IDictionary dict = null; reader.Read(); while (!(reader.NodeType == XmlNodeType.EndElement)) { if (reader.NodeType == XmlNodeType.Element && !reader.IsEmptyElement) { if (dict == null) { dict = new HybridDictionary(); } dict.Add(reader.LocalName, reader.ReadString()); } reader.Skip(); } if (dict != null) { adDicts.Add(dict); } } } AdRec [] adRecs = SetAdRecs(adDicts); return adRecs; } /// /// Loads the given XML stream into an array of AdRec structures /// private AdRec [] LoadStream(Stream stream) { AdRec [] adRecs = null; try { // Read the XML stream into an array of dictionaries XmlReader reader = XmlUtils.CreateXmlReader(stream); // Perf: We use LoadFromXmlReader instead of LoadXmlDocument to // do the text parsing only once adRecs = LoadFromXmlReader(reader); } catch (Exception e) { throw new HttpException( SR.GetString(SR.AdRotator_parse_error, ID, e.Message), e); } if (adRecs == null) { throw new HttpException( SR.GetString(SR.AdRotator_no_advertisements, ID, AdvertisementFile)); } return adRecs; } private AdRec [] LoadXmlDocument(XmlDocument doc) { // Read the XML data into an array of dictionaries ArrayList adDicts = new ArrayList(); if (doc.DocumentElement != null && doc.DocumentElement.LocalName == XmlDocumentTag) { XmlNode elem = doc.DocumentElement.FirstChild; while (elem != null) { IDictionary dict = null; if (elem.LocalName.Equals(XmlAdTag)) { XmlNode prop = elem.FirstChild; while (prop != null) { if (prop.NodeType == XmlNodeType.Element) { if (dict == null) { dict = new HybridDictionary(); } dict.Add(prop.LocalName, prop.InnerText); } prop = prop.NextSibling; } } if (dict != null) { adDicts.Add(dict); } elem = elem.NextSibling; } } AdRec [] adRecs = SetAdRecs(adDicts); return adRecs; } /// /// Used to determine if the advertisement meets current criteria. Does a comparison with /// KeywordFilter if it is set. /// private bool MatchingAd(AdRec adRec, string keywordFilter) { Debug.Assert(keywordFilter != null && keywordFilter.Length > 0); return(String.Equals(keywordFilter, adRec.keyword, StringComparison.OrdinalIgnoreCase)); } /// /// Raises the event for an . /// protected virtual void OnAdCreated(AdCreatedEventArgs e) { AdCreatedEventHandler handler = (AdCreatedEventHandler)Events[EventAdCreated]; if (handler != null) handler(this, e); } protected internal override void OnInit(EventArgs e) { base.OnInit(e); // VSWhidbey 419600: We just always need binding data every time since // AdRotator doesn't store the entire Ad data in ViewState for selecting // Ad during postbacks. It's too big for storing in ViewState. RequiresDataBinding = true; } /// /// /// Gets the advertisement information for rendering in its parameter, then calls /// the OnAdCreated event to render the ads. /// protected internal override void OnPreRender(EventArgs e) { base.OnPreRender(e); // If after PreRender (which would call DataBind if DataSource or DataSourceID available) // and no _adRecs created, it must be the normal v1 behavior which uses ad file. if (_adRecs == null && AdvertisementFile.Length > 0) { PerformAdFileBinding(); } // If handler is specified, we don't do any post-cache // substitution because the handler code would not be executed. // // VSWhidbey 213759: We also don't want any post-cache substitution // if OnAdCreated has been overridden if (Events[EventAdCreated] != null || IsOnAdCreatedOverridden()) { // Fire the user event for further customization SelectedAdArgs = GetAdCreatedEventArgs(); OnAdCreated(SelectedAdArgs); } } private void PerformAdFileBinding() { // Getting ad data from physical file is V1 way which is not supported // by the base class DataBoundControl so we had above code to handle // this case. However, we need to support DataBound control events // in Whidbey and since above code doesn't go through the event // raising in the base class DataBoundControl, here we mimic them. OnDataBinding(EventArgs.Empty); // get the ads from the file or app cache _adRecs = GetFileData(AdvertisementFile); OnDataBound(EventArgs.Empty); } protected internal override void PerformDataBinding(IEnumerable data) { if (data != null) { // We retrieve ad data from xml format in a specific way. XmlDataSource xmlDataSource = null; object dataSource = DataSource; if (dataSource != null) { xmlDataSource = dataSource as XmlDataSource; } else { // DataSourceID case, we know that only one source is available xmlDataSource = GetDataSource() as XmlDataSource; } if (xmlDataSource != null) { _adRecs = GetXmlDataSourceData(xmlDataSource); } else { _adRecs = GetDataSourceData(data); } } } protected override void PerformSelect() { // VSWhidbey 141362 CheckOnlyOneDataSource(); if (AdvertisementFile.Length > 0) { PerformAdFileBinding(); } else { base.PerformSelect(); } } // internal AdCreatedEventArgs PickAd() { AdCreatedEventArgs adArgs = SelectedAdArgs; if (adArgs == null) { adArgs = GetAdCreatedEventArgs(); } adArgs.ImageUrl = ResolveAdRotatorUrl(BaseUrl, adArgs.ImageUrl); adArgs.NavigateUrl = ResolveAdRotatorUrl(BaseUrl, adArgs.NavigateUrl); return adArgs; } /// /// /// Displays the on the client. /// protected internal override void Render(HtmlTextWriter writer) { if (!DesignMode && !IsPostCacheAdHelper && DoPostCacheSubstitutionAsNeeded(writer)) { return; } AdCreatedEventArgs adArgs = PickAd(); RenderLink(writer, adArgs); } private void RenderLink(HtmlTextWriter writer, AdCreatedEventArgs adArgs) { Debug.Assert(writer != null); Debug.Assert(adArgs != null); HyperLink bannerLink = new HyperLink(); bannerLink.NavigateUrl = adArgs.NavigateUrl; bannerLink.Target = Target; if (HasAttributes) { foreach(string key in Attributes.Keys) { bannerLink.Attributes[key] = Attributes[key]; } } string id = ID; if (!String.IsNullOrEmpty(id)) { bannerLink.ID = ClientID; } if (!Enabled) { bannerLink.Enabled = false; } // WebControl's properties use a private flag to determine if a // property is set and does not return the value unless the flag is // marked. So here we access those properites (inherited from WebControl) // directly from the ViewState bag because if ViewState bag reference // was copied to the helper class in the optimized case during the // Initialize() method, the flags of the properties wouldn't be set // in the helper class. string accessKey = (string) ViewState["AccessKey"]; if (!String.IsNullOrEmpty(accessKey)) { bannerLink.AccessKey = accessKey; } object o = ViewState["TabIndex"]; if (o != null) { short tabIndex = (short) o; if (tabIndex != (short) 0) { bannerLink.TabIndex = tabIndex; } } bannerLink.RenderBeginTag(writer); // create inner Image Image bannerImage = new Image(); // apply styles to image if (ControlStyleCreated) { bannerImage.ApplyStyle(ControlStyle); } string alternateText = adArgs.AlternateText; if (!String.IsNullOrEmpty(alternateText)) { bannerImage.AlternateText = alternateText; } else { // 25914 Do not render empty 'alt' attribute if tag is never specified IDictionary adProps = adArgs.AdProperties; string altTextKey = (AlternateTextField.Length != 0) ? AlternateTextField : AdCreatedEventArgs.AlternateTextElement; string altText = (adProps == null) ? null : (string) adProps[altTextKey]; if (altText != null && altText.Length == 0) { bannerImage.GenerateEmptyAlternateText = true; } } // Perf work: AdRotator should have resolved the NavigateUrl and // ImageUrl when assigning them and have UrlResolved set properly. bannerImage.UrlResolved = true; string imageUrl = adArgs.ImageUrl; if (!String.IsNullOrEmpty(imageUrl)) { bannerImage.ImageUrl = imageUrl; } if (adArgs.HasWidth) { bannerImage.ControlStyle.Width = adArgs.Width; } if (adArgs.HasHeight) { bannerImage.ControlStyle.Height = adArgs.Height; } string toolTip = (string) ViewState["ToolTip"]; if (!String.IsNullOrEmpty(toolTip)) { bannerImage.ToolTip = toolTip; } bannerImage.RenderControl(writer); bannerLink.RenderEndTag(writer); } private string ResolveAdRotatorUrl(string baseUrl, string relativeUrl) { if ((relativeUrl == null) || (relativeUrl.Length == 0) || (UrlPath.IsRelativeUrl(relativeUrl) == false) || (baseUrl == null) || (baseUrl.Length == 0)) { return relativeUrl; } // make it absolute return UrlPath.Combine(baseUrl, relativeUrl); } /// /// Selects an advertisement from the a list of records based /// on different factors. /// private IDictionary SelectAdFromRecords() { if (_adRecs == null || _adRecs.Length == 0) { return null; } string keywordFilter = KeywordFilter; bool noKeywordFilter = String.IsNullOrEmpty(keywordFilter); if (!noKeywordFilter) { // do a lower case comparison keywordFilter = keywordFilter.ToLower(CultureInfo.InvariantCulture); } // sum the matching impressions int totalImpressions = 0; for (int i = 0; i < _adRecs.Length; i++) { if (noKeywordFilter || MatchingAd(_adRecs[i], keywordFilter)) { totalImpressions += _adRecs[i].impressions; } } if (totalImpressions == 0) { return null; } // select one using a random number between 1 and totalImpressions int selectedImpression = GetRandomNumber(totalImpressions); int impressionCounter = 0; int selectedIndex = -1; for (int i = 0; i < _adRecs.Length; i++) { // Is this the ad? if (noKeywordFilter || MatchingAd(_adRecs[i], keywordFilter)) { impressionCounter += _adRecs[i].impressions; if (selectedImpression <= impressionCounter) { selectedIndex = i; break; } } } Debug.Assert(selectedIndex >= 0 && selectedIndex < _adRecs.Length, "Index not found"); return _adRecs[selectedIndex].adProperties; } private AdRec [] SetAdRecs(ArrayList adDicts) { if (adDicts == null || adDicts.Count == 0) { return null; } // Create an array of AdRec structures from the dictionaries, removing blanks AdRec [] adRecs = new AdRec[adDicts.Count]; int iRec = 0; for (int i = 0; i < adDicts.Count; i++) { if (adDicts[i] != null) { adRecs[iRec].Initialize((IDictionary) adDicts[i]); iRec++; } } Debug.Assert(iRec == adDicts.Count, "Record count did not match non-null entries"); return adRecs; } /// /// Structure to store ads in memory for fast selection by multiple instances of adrotator /// Stores the dictionary and caches some values for easier selection. /// private struct AdRec { public string keyword; public int impressions; public IDictionary adProperties; /// /// Initialize the stuct based on a dictionary containing the advertisement properties /// public void Initialize(IDictionary adProperties) { // Initialize the values we need to keep for ad selection Debug.Assert(adProperties != null, "Required here"); this.adProperties = adProperties; // remove null and trim keyword for easier comparisons. // VSWhidbey 114634: Be defensive and only retrieve the keyword // value if it is in string type object keywordValue = adProperties[KeywordProperty]; if (keywordValue != null && keywordValue is string) { keyword = ((string) keywordValue).Trim(); } else { keyword = string.Empty; } // get the impressions, but be defensive: let the schema enforce the rules. Default to 1. string impressionsString = adProperties[ImpressionsProperty] as string; if (String.IsNullOrEmpty(impressionsString) || !int.TryParse(impressionsString, NumberStyles.Integer, CultureInfo.InvariantCulture, out impressions)) { impressions = 1; } if (impressions < 0) { impressions = 1; } } } } }