summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--projecttemplates/WebFormsRelyingParty/Web.config16
-rw-r--r--samples/InfoCardRelyingParty/web.config21
-rw-r--r--samples/OAuthConsumer/Web.config7
-rw-r--r--samples/OAuthConsumerWpf/App.config7
-rw-r--r--samples/OAuthServiceProvider/Web.config7
-rw-r--r--samples/OpenIdOfflineProvider/App.config13
-rw-r--r--samples/OpenIdProviderMvc/Web.config10
-rw-r--r--samples/OpenIdProviderWebForms/Web.config10
-rw-r--r--samples/OpenIdRelyingPartyMvc/Web.config10
-rw-r--r--samples/OpenIdRelyingPartyWebForms/Web.config3
-rw-r--r--src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd11
-rw-r--r--src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs14
-rw-r--r--src/DotNetOpenAuth/Configuration/ReportingElement.cs139
-rw-r--r--src/DotNetOpenAuth/DotNetOpenAuth.csproj2
-rw-r--r--src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs1
-rw-r--r--src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs10
-rw-r--r--src/DotNetOpenAuth/Messaging/MessagingUtilities.cs1
-rw-r--r--src/DotNetOpenAuth/OAuth/ConsumerBase.cs2
-rw-r--r--src/DotNetOpenAuth/OAuth/ServiceProvider.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/Behaviors/GsaIcamProfile.cs4
-rw-r--r--src/DotNetOpenAuth/OpenId/Behaviors/PpidGeneration.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs4
-rw-r--r--src/DotNetOpenAuth/OpenId/Interop/OpenIdRelyingPartyShim.cs7
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/AuthenticationRequest.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/HostProcessedRequest.cs1
-rw-r--r--src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs4
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs13
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs1
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs3
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs1
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs2
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/SelectorInfoCardButton.cs1
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs1
-rw-r--r--src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs1
-rw-r--r--src/DotNetOpenAuth/Properties/AssemblyInfo.cs4
-rw-r--r--src/DotNetOpenAuth/Reporting.cs806
-rw-r--r--src/DotNetOpenAuth/XrdsPublisher.cs7
-rw-r--r--src/DotNetOpenAuth/Yadis/Yadis.cs3
39 files changed, 1118 insertions, 37 deletions
diff --git a/projecttemplates/WebFormsRelyingParty/Web.config b/projecttemplates/WebFormsRelyingParty/Web.config
index f0b01ff..1d7c29f 100644
--- a/projecttemplates/WebFormsRelyingParty/Web.config
+++ b/projecttemplates/WebFormsRelyingParty/Web.config
@@ -1,4 +1,12 @@
<?xml version="1.0"?>
+<!--
+ Note: As an alternative to hand editing this file you can use the
+ web admin tool to configure settings for your application. Use
+ the Website->Asp.Net Configuration option in Visual Studio.
+ A full list of settings and comments can be found in
+ machine.config.comments usually located in
+ \Windows\Microsoft.Net\Framework\v2.x\Config
+-->
<configuration>
<configSections>
<section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
@@ -16,6 +24,7 @@
</sectionGroup>
</sectionGroup>
</configSections>
+
<!-- The uri section is necessary to turn on .NET 3.5 support for IDN (international domain names),
which is necessary for OpenID urls with unicode characters in the domain/host name.
It is also required to put the Uri class into RFC 3986 escaping mode, which OpenID and OAuth require. -->
@@ -23,6 +32,7 @@
<idn enabled="All" />
<iriParsing enabled="true" />
</uri>
+
<system.net>
<defaultProxy enabled="true" />
<settings>
@@ -32,6 +42,7 @@
<servicePointManager checkCertificateRevocationList="true" />
</settings>
</system.net>
+
<!-- this is an optional configuration section where aspects of dotnetopenauth can be customized -->
<dotNetOpenAuth>
<messaging>
@@ -56,7 +67,10 @@
<store type="RelyingPartyLogic.NonceDbStore, RelyingPartyLogic"/>
</serviceProvider>
</oauth>
+ <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
+ <reporting enabled="true" />
</dotNetOpenAuth>
+
<!-- log4net is a 3rd party (free) logger library that DotNetOpenAuth will use if present but does not require. -->
<log4net>
<appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender">
@@ -249,4 +263,4 @@
</authorization>
</system.web>
</location>
-</configuration> \ No newline at end of file
+</configuration>
diff --git a/samples/InfoCardRelyingParty/web.config b/samples/InfoCardRelyingParty/web.config
index f14d14b..d91f9e8 100644
--- a/samples/InfoCardRelyingParty/web.config
+++ b/samples/InfoCardRelyingParty/web.config
@@ -1,15 +1,8 @@
<?xml version="1.0"?>
-<!--
- Note: As an alternative to hand editing this file you can use the
- web admin tool to configure settings for your application. Use
- the Website->Asp.Net Configuration option in Visual Studio.
- A full list of settings and comments can be found in
- machine.config.comments usually located in
- \Windows\Microsoft.Net\Framework\v2.x\Config
--->
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler" requirePermission="false" />
+ <section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection" requirePermission="false" allowLocation="true"/>
<sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/>
@@ -23,6 +16,12 @@
</sectionGroup>
</configSections>
+ <!-- this is an optional configuration section where aspects of dotnetopenauth can be customized -->
+ <dotNetOpenAuth>
+ <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
+ <reporting enabled="true" />
+ </dotNetOpenAuth>
+
<log4net>
<appender name="TracePageAppender" type="TracePageAppender, __code">
<layout type="log4net.Layout.PatternLayout">
@@ -41,13 +40,13 @@
</logger>
</log4net>
- <appSettings/>
- <connectionStrings/>
-
<system.net>
<defaultProxy enabled="true" />
</system.net>
+ <appSettings/>
+ <connectionStrings/>
+
<system.web>
<!--
Set compilation debug="true" to insert debugging
diff --git a/samples/OAuthConsumer/Web.config b/samples/OAuthConsumer/Web.config
index c3a808a..9467175 100644
--- a/samples/OAuthConsumer/Web.config
+++ b/samples/OAuthConsumer/Web.config
@@ -3,6 +3,7 @@
<configSections>
<section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler" requirePermission="false" />
+ <section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection" requirePermission="false" allowLocation="true"/>
<sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/>
@@ -34,6 +35,12 @@
</settings>
</system.net>
+ <!-- this is an optional configuration section where aspects of dotnetopenauth can be customized -->
+ <dotNetOpenAuth>
+ <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
+ <reporting enabled="true" />
+ </dotNetOpenAuth>
+
<appSettings>
<!-- Fill in your various consumer keys and secrets here to make the sample work. -->
<!-- You must get these values by signing up with each individual service provider. -->
diff --git a/samples/OAuthConsumerWpf/App.config b/samples/OAuthConsumerWpf/App.config
index e53b4a3..f142405 100644
--- a/samples/OAuthConsumerWpf/App.config
+++ b/samples/OAuthConsumerWpf/App.config
@@ -3,6 +3,7 @@
<configSections>
<section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler" requirePermission="false" />
+ <section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection" requirePermission="false" allowLocation="true"/>
</configSections>
<!-- The uri section is necessary to turn on .NET 3.5 support for IDN (international domain names),
@@ -23,6 +24,12 @@
</settings>
</system.net>
+ <!-- this is an optional configuration section where aspects of dotnetopenauth can be customized -->
+ <dotNetOpenAuth>
+ <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
+ <reporting enabled="true"/>
+ </dotNetOpenAuth>
+
<appSettings>
<!-- Fill in your various consumer keys and secrets here to make the sample work. -->
<!-- You must get these values by signing up with each individual service provider. -->
diff --git a/samples/OAuthServiceProvider/Web.config b/samples/OAuthServiceProvider/Web.config
index d039daa..c21ebd4 100644
--- a/samples/OAuthServiceProvider/Web.config
+++ b/samples/OAuthServiceProvider/Web.config
@@ -3,6 +3,7 @@
<configSections>
<section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler" requirePermission="false"/>
+ <section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection" requirePermission="false" allowLocation="true"/>
<sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" allowDefinition="MachineToApplication"/>
@@ -34,6 +35,12 @@
</settings>
</system.net>
+ <!-- this is an optional configuration section where aspects of dotnetopenauth can be customized -->
+ <dotNetOpenAuth>
+ <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
+ <reporting enabled="true" />
+ </dotNetOpenAuth>
+
<appSettings/>
<connectionStrings>
<add name="DatabaseConnectionString" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Database.mdf;Integrated Security=True;User Instance=True"
diff --git a/samples/OpenIdOfflineProvider/App.config b/samples/OpenIdOfflineProvider/App.config
index cd04b13..7263338 100644
--- a/samples/OpenIdOfflineProvider/App.config
+++ b/samples/OpenIdOfflineProvider/App.config
@@ -1,9 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
+ <section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
<section name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection, DotNetOpenAuth" requirePermission="false" allowLocation="true"/>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" requirePermission="false"/>
</configSections>
+
+ <!-- The uri section is necessary to turn on .NET 3.5 support for IDN (international domain names),
+ which is necessary for OpenID urls with unicode characters in the domain/host name.
+ It is also required to put the Uri class into RFC 3986 escaping mode, which OpenID and OAuth require. -->
+ <uri>
+ <idn enabled="All"/>
+ <iriParsing enabled="true"/>
+ </uri>
+
+ <!-- this is an optional configuration section where aspects of dotnetopenauth can be customized -->
<dotNetOpenAuth>
<messaging>
<untrustedWebRequest>
@@ -13,6 +24,8 @@
</whitelistHosts>
</untrustedWebRequest>
</messaging>
+ <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
+ <reporting enabled="true"/>
</dotNetOpenAuth>
<log4net>
<appender name="TextBoxAppender" type="log4net.Appender.TextWriterAppender">
diff --git a/samples/OpenIdProviderMvc/Web.config b/samples/OpenIdProviderMvc/Web.config
index f36bfcf..93dbf58 100644
--- a/samples/OpenIdProviderMvc/Web.config
+++ b/samples/OpenIdProviderMvc/Web.config
@@ -1,12 +1,4 @@
<?xml version="1.0"?>
-<!--
- Note: As an alternative to hand editing this file you can use the
- web admin tool to configure settings for your application. Use
- the Website->Asp.Net Configuration option in Visual Studio.
- A full list of settings and comments can be found in
- machine.config.comments usually located in
- \Windows\Microsoft.Net\Framework\v2.x\Config
--->
<configuration>
<configSections>
<section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
@@ -67,6 +59,8 @@
</whitelistHosts>
</untrustedWebRequest>
</messaging>
+ <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
+ <reporting enabled="true" />
</dotNetOpenAuth>
<appSettings/>
diff --git a/samples/OpenIdProviderWebForms/Web.config b/samples/OpenIdProviderWebForms/Web.config
index 159dcd1..a662be3 100644
--- a/samples/OpenIdProviderWebForms/Web.config
+++ b/samples/OpenIdProviderWebForms/Web.config
@@ -1,12 +1,4 @@
<?xml version="1.0"?>
-<!--
- Note: As an alternative to hand editing this file you can use the
- web admin tool to configure settings for your application. Use
- the Website->Asp.Net Configuration option in Visual Studio.
- A full list of settings and comments can be found in
- machine.config.comments usually located in
- \Windows\Microsoft.Net\Framework\v2.x\Config
--->
<configuration>
<configSections>
<section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
@@ -66,6 +58,8 @@
</whitelistHosts>
</untrustedWebRequest>
</messaging>
+ <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
+ <reporting enabled="true" />
</dotNetOpenAuth>
<system.web>
diff --git a/samples/OpenIdRelyingPartyMvc/Web.config b/samples/OpenIdRelyingPartyMvc/Web.config
index c3bfa41..315eaa9 100644
--- a/samples/OpenIdRelyingPartyMvc/Web.config
+++ b/samples/OpenIdRelyingPartyMvc/Web.config
@@ -1,12 +1,4 @@
<?xml version="1.0"?>
-<!--
- Note: As an alternative to hand editing this file you can use the
- web admin tool to configure settings for your application. Use
- the Website->Asp.Net Configuration option in Visual Studio.
- A full list of settings and comments can be found in
- machine.config.comments usually located in
- \Windows\Microsoft.Net\Framework\v2.x\Config
--->
<configuration>
<configSections>
<section name="uri" type="System.Configuration.UriSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
@@ -62,6 +54,8 @@
</whitelistHosts>
</untrustedWebRequest>
</messaging>
+ <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
+ <reporting enabled="true" />
</dotNetOpenAuth>
<appSettings/>
diff --git a/samples/OpenIdRelyingPartyWebForms/Web.config b/samples/OpenIdRelyingPartyWebForms/Web.config
index c48c198..0eea738 100644
--- a/samples/OpenIdRelyingPartyWebForms/Web.config
+++ b/samples/OpenIdRelyingPartyWebForms/Web.config
@@ -23,6 +23,7 @@
<!--<servicePointManager checkCertificateRevocationList="true"/>-->
</settings>
</system.net>
+
<!-- this is an optional configuration section where aspects of dotnetopenauth can be customized -->
<dotNetOpenAuth>
<openid>
@@ -46,6 +47,8 @@
</whitelistHosts>
</untrustedWebRequest>
</messaging>
+ <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. -->
+ <reporting enabled="true" />
</dotNetOpenAuth>
<appSettings>
diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd
index a637d1f..63ad100 100644
--- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd
+++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd
@@ -313,6 +313,17 @@
</xs:choice>
</xs:complexType>
</xs:element>
+ <xs:element name="reporting">
+ <xs:complexType>
+ <xs:attribute name="enabled" type="xs:boolean" />
+ <xs:attribute name="minimumReportingInterval" type="xs:string" />
+ <xs:attribute name="minimumFlushInterval" type="xs:string" />
+ <xs:attribute name="includeFeatureUsage" type="xs:boolean" default="true" />
+ <xs:attribute name="includeEventStatistics" type="xs:boolean" default="true" />
+ <xs:attribute name="includeLocalRequestUris" type="xs:boolean" default="true" />
+ <xs:attribute name="includeCultures" type="xs:boolean" default="true" />
+ </xs:complexType>
+ </xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs b/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs
index 7bd84d9..aa956d1 100644
--- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs
+++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs
@@ -35,6 +35,11 @@ namespace DotNetOpenAuth.Configuration {
private const string OAuthElementName = "oauth";
/// <summary>
+ /// The name of the &lt;reporting&gt; sub-element.
+ /// </summary>
+ private const string ReportingElementName = "reporting";
+
+ /// <summary>
/// Initializes a new instance of the <see cref="DotNetOpenAuthSection"/> class.
/// </summary>
internal DotNetOpenAuthSection() {
@@ -75,5 +80,14 @@ namespace DotNetOpenAuth.Configuration {
get { return (OAuthElement)this[OAuthElementName] ?? new OAuthElement(); }
set { this[OAuthElementName] = value; }
}
+
+ /// <summary>
+ /// Gets or sets the configuration for reporting.
+ /// </summary>
+ [ConfigurationProperty(ReportingElementName)]
+ internal ReportingElement Reporting {
+ get { return (ReportingElement)this[ReportingElementName] ?? new ReportingElement(); }
+ set { this[ReportingElementName] = value; }
+ }
}
}
diff --git a/src/DotNetOpenAuth/Configuration/ReportingElement.cs b/src/DotNetOpenAuth/Configuration/ReportingElement.cs
new file mode 100644
index 0000000..ab1437f
--- /dev/null
+++ b/src/DotNetOpenAuth/Configuration/ReportingElement.cs
@@ -0,0 +1,139 @@
+//-----------------------------------------------------------------------
+// <copyright file="ReportingElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Configuration {
+ using System;
+ using System.Collections.Generic;
+ using System.Configuration;
+ using System.Linq;
+ using System.Text;
+
+ /// <summary>
+ /// Represents the &lt;reporting&gt; element in the host's .config file.
+ /// </summary>
+ internal class ReportingElement : ConfigurationElement {
+ /// <summary>
+ /// The name of the @enabled attribute.
+ /// </summary>
+ private const string EnabledAttributeName = "enabled";
+
+ /// <summary>
+ /// The name of the @minimumReportingInterval attribute.
+ /// </summary>
+ private const string MinimumReportingIntervalAttributeName = "minimumReportingInterval";
+
+ /// <summary>
+ /// The name of the @minimumFlushInterval attribute.
+ /// </summary>
+ private const string MinimumFlushIntervalAttributeName = "minimumFlushInterval";
+
+ /// <summary>
+ /// The name of the @includeFeatureUsage attribute.
+ /// </summary>
+ private const string IncludeFeatureUsageAttributeName = "includeFeatureUsage";
+
+ /// <summary>
+ /// The name of the @includeEventStatistics attribute.
+ /// </summary>
+ private const string IncludeEventStatisticsAttributeName = "includeEventStatistics";
+
+ /// <summary>
+ /// The name of the @includeLocalRequestUris attribute.
+ /// </summary>
+ private const string IncludeLocalRequestUrisAttributeName = "includeLocalRequestUris";
+
+ /// <summary>
+ /// The name of the @includeCultures attribute.
+ /// </summary>
+ private const string IncludeCulturesAttributeName = "includeCultures";
+
+ /// <summary>
+ /// The default value for the @minimumFlushInterval attribute.
+ /// </summary>
+#if DEBUG
+ private const string MinimumFlushIntervalDefault = "0";
+#else
+ private const string MinimumFlushIntervalDefault = "0:15";
+#endif
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReportingElement"/> class.
+ /// </summary>
+ internal ReportingElement() {
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this reporting is enabled.
+ /// </summary>
+ /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value>
+ [ConfigurationProperty(EnabledAttributeName, DefaultValue = false)]
+ internal bool Enabled {
+ get { return (bool)this[EnabledAttributeName]; }
+ set { this[EnabledAttributeName] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum frequency that reports will be published.
+ /// </summary>
+ [ConfigurationProperty(MinimumReportingIntervalAttributeName, DefaultValue = "1")] // 1 day default
+ internal TimeSpan MinimumReportingInterval {
+ get { return (TimeSpan)this[MinimumReportingIntervalAttributeName]; }
+ set { this[MinimumReportingIntervalAttributeName] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets the maximum frequency the set can be flushed to disk.
+ /// </summary>
+ [ConfigurationProperty(MinimumFlushIntervalAttributeName, DefaultValue = MinimumFlushIntervalDefault)]
+ internal TimeSpan MinimumFlushInterval {
+ get { return (TimeSpan)this[MinimumFlushIntervalAttributeName]; }
+ set { this[MinimumFlushIntervalAttributeName] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to include a list of library features used in the report.
+ /// </summary>
+ /// <value><c>true</c> to include a report of features used; otherwise, <c>false</c>.</value>
+ [ConfigurationProperty(IncludeFeatureUsageAttributeName, DefaultValue = true)]
+ internal bool IncludeFeatureUsage {
+ get { return (bool)this[IncludeFeatureUsageAttributeName]; }
+ set { this[IncludeFeatureUsageAttributeName] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to include statistics of certain events such as
+ /// authentication success and failure counting, and can include remote endpoint URIs.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> to include event counters in the report; otherwise, <c>false</c>.
+ /// </value>
+ [ConfigurationProperty(IncludeEventStatisticsAttributeName, DefaultValue = true)]
+ internal bool IncludeEventStatistics {
+ get { return (bool)this[IncludeEventStatisticsAttributeName]; }
+ set { this[IncludeEventStatisticsAttributeName] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to include a few URLs to pages on the hosting
+ /// web site that host DotNetOpenAuth components.
+ /// </summary>
+ [ConfigurationProperty(IncludeLocalRequestUrisAttributeName, DefaultValue = true)]
+ internal bool IncludeLocalRequestUris {
+ get { return (bool)this[IncludeLocalRequestUrisAttributeName]; }
+ set { this[IncludeLocalRequestUrisAttributeName] = value; }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to include the cultures requested by the user agent
+ /// on pages that host DotNetOpenAuth components.
+ /// </summary>
+ [ConfigurationProperty(IncludeCulturesAttributeName, DefaultValue = true)]
+ internal bool IncludeCultures {
+ get { return (bool)this[IncludeCulturesAttributeName]; }
+ set { this[IncludeCulturesAttributeName] = value; }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
index 42b99fd..4b73c05 100644
--- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj
+++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj
@@ -229,6 +229,7 @@ http://opensource.org/licenses/ms-pl.html
<Compile Include="Configuration\OpenIdProviderSecuritySettingsElement.cs" />
<Compile Include="Configuration\OpenIdRelyingPartyElement.cs" />
<Compile Include="Configuration\OpenIdRelyingPartySecuritySettingsElement.cs" />
+ <Compile Include="Configuration\ReportingElement.cs" />
<Compile Include="Configuration\TypeConfigurationCollection.cs" />
<Compile Include="Configuration\TypeConfigurationElement.cs" />
<Compile Include="Configuration\UntrustedWebRequestElement.cs" />
@@ -556,6 +557,7 @@ http://opensource.org/licenses/ms-pl.html
<Compile Include="OAuth\ChannelElements\RsaSha1SigningBindingElement.cs" />
<Compile Include="Messaging\StandardWebRequestHandler.cs" />
<Compile Include="Messaging\MessageReceivingEndpoint.cs" />
+ <Compile Include="Reporting.cs" />
<Compile Include="Util.cs" />
<Compile Include="OAuth\Protocol.cs" />
<Compile Include="OAuth\ServiceProvider.cs" />
diff --git a/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs b/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs
index de4d023..86c1118 100644
--- a/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs
+++ b/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs
@@ -199,6 +199,7 @@ namespace DotNetOpenAuth.InfoCard {
/// </summary>
public InfoCardSelector() {
this.ToolTip = InfoCardStrings.SelectorClickPrompt;
+ Reporting.RecordFeatureUse(this);
}
/// <summary>
diff --git a/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs
index 2951514..6c7f2f9 100644
--- a/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs
+++ b/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs
@@ -73,6 +73,8 @@ namespace DotNetOpenAuth.Messaging {
// these as well.
this.form = request.Form;
this.queryString = request.QueryString;
+
+ Reporting.RecordRequestStatistics(this);
}
/// <summary>
@@ -96,6 +98,8 @@ namespace DotNetOpenAuth.Messaging {
this.RawUrl = rawUrl;
this.Headers = headers;
this.InputStream = inputStream;
+
+ Reporting.RecordRequestStatistics(this);
}
/// <summary>
@@ -115,6 +119,8 @@ namespace DotNetOpenAuth.Messaging {
}
this.InputStream = listenerRequest.InputStream;
+
+ Reporting.RecordRequestStatistics(this);
}
/// <summary>
@@ -131,6 +137,8 @@ namespace DotNetOpenAuth.Messaging {
this.Url = requestUri;
this.UrlBeforeRewriting = requestUri;
this.RawUrl = MakeUpRawUrlFromUrl(requestUri);
+
+ Reporting.RecordRequestStatistics(this);
}
/// <summary>
@@ -157,6 +165,8 @@ namespace DotNetOpenAuth.Messaging {
this.RawUrl = MakeUpRawUrlFromUrl(request.RequestUri);
this.Headers = GetHeaderCollection(request.Headers);
this.InputStream = null;
+
+ Reporting.RecordRequestStatistics(this);
}
/// <summary>
diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
index e1e3f59..3f77a25 100644
--- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
+++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs
@@ -152,6 +152,7 @@ namespace DotNetOpenAuth.Messaging {
Contract.Requires<ArgumentNullException>(requestHandler != null);
Contract.Requires<ArgumentNullException>(parts != null);
+ Reporting.RecordFeatureUse("MessagingUtilities.PostMultipart");
string boundary = Guid.NewGuid().ToString();
string partLeadingBoundary = string.Format(CultureInfo.InvariantCulture, "\r\n--{0}\r\n", boundary);
string finalTrailingBoundary = string.Format(CultureInfo.InvariantCulture, "\r\n--{0}--\r\n", boundary);
diff --git a/src/DotNetOpenAuth/OAuth/ConsumerBase.cs b/src/DotNetOpenAuth/OAuth/ConsumerBase.cs
index b9d4718..6c0ce42 100644
--- a/src/DotNetOpenAuth/OAuth/ConsumerBase.cs
+++ b/src/DotNetOpenAuth/OAuth/ConsumerBase.cs
@@ -34,6 +34,8 @@ namespace DotNetOpenAuth.OAuth {
this.OAuthChannel = new OAuthChannel(signingElement, store, tokenManager);
this.ServiceProvider = serviceDescription;
this.SecuritySettings = DotNetOpenAuthSection.Configuration.OAuth.Consumer.SecuritySettings.CreateSecuritySettings();
+
+ Reporting.RecordFeatureAndDependencyUse(this, serviceDescription, tokenManager, null);
}
/// <summary>
diff --git a/src/DotNetOpenAuth/OAuth/ServiceProvider.cs b/src/DotNetOpenAuth/OAuth/ServiceProvider.cs
index e2c82bb..829b572 100644
--- a/src/DotNetOpenAuth/OAuth/ServiceProvider.cs
+++ b/src/DotNetOpenAuth/OAuth/ServiceProvider.cs
@@ -102,6 +102,8 @@ namespace DotNetOpenAuth.OAuth {
this.OAuthChannel = new OAuthChannel(signingElement, nonceStore, tokenManager, messageTypeProvider);
this.TokenGenerator = new StandardTokenGenerator();
this.SecuritySettings = DotNetOpenAuthSection.Configuration.OAuth.ServiceProvider.SecuritySettings.CreateSecuritySettings();
+
+ Reporting.RecordFeatureAndDependencyUse(this, serviceDescription, tokenManager, nonceStore);
}
/// <summary>
diff --git a/src/DotNetOpenAuth/OpenId/Behaviors/GsaIcamProfile.cs b/src/DotNetOpenAuth/OpenId/Behaviors/GsaIcamProfile.cs
index 23377c8..20c207f 100644
--- a/src/DotNetOpenAuth/OpenId/Behaviors/GsaIcamProfile.cs
+++ b/src/DotNetOpenAuth/OpenId/Behaviors/GsaIcamProfile.cs
@@ -114,6 +114,8 @@ namespace DotNetOpenAuth.OpenId.Behaviors {
!requestInternal.AppliedExtensions.OfType<FetchRequest>().Any()),
BehaviorStrings.PiiIncludedWithNoPiiPolicy);
}
+
+ Reporting.RecordEventOccurrence(this, "RP");
}
/// <summary>
@@ -256,6 +258,8 @@ namespace DotNetOpenAuth.OpenId.Behaviors {
}
}
}
+
+ Reporting.RecordEventOccurrence(this, "OP");
}
return result;
diff --git a/src/DotNetOpenAuth/OpenId/Behaviors/PpidGeneration.cs b/src/DotNetOpenAuth/OpenId/Behaviors/PpidGeneration.cs
index baef943..a465611 100644
--- a/src/DotNetOpenAuth/OpenId/Behaviors/PpidGeneration.cs
+++ b/src/DotNetOpenAuth/OpenId/Behaviors/PpidGeneration.cs
@@ -108,6 +108,8 @@ namespace DotNetOpenAuth.OpenId.Behaviors {
if (!papeResponse.ActualPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) {
papeResponse.ActualPolicies.Add(AuthenticationPolicies.PrivatePersonalIdentifier);
}
+
+ Reporting.RecordEventOccurrence(this, string.Empty);
}
}
diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs
index 3f07e5a..4fa70a0 100644
--- a/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs
+++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs
@@ -88,6 +88,8 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
foreach (IExtensionMessage protocolExtension in extendableMessage.Extensions) {
var extension = protocolExtension as IOpenIdMessageExtension;
if (extension != null) {
+ Reporting.RecordFeatureUse(protocolExtension);
+
// Give extensions that require custom serialization a chance to do their work.
var customSerializingExtension = extension as IMessageWithEvents;
if (customSerializingExtension != null) {
@@ -145,6 +147,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
if (extendableMessage != null) {
// First add the extensions that are signed by the Provider.
foreach (IOpenIdMessageExtension signedExtension in this.GetExtensions(extendableMessage, true, null)) {
+ Reporting.RecordFeatureUse(signedExtension);
signedExtension.IsSignedByRemoteParty = true;
extendableMessage.Extensions.Add(signedExtension);
}
@@ -154,6 +157,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
if (this.relyingPartySecuritySettings == null || !this.relyingPartySecuritySettings.IgnoreUnsignedExtensions) {
Func<string, bool> isNotSigned = typeUri => !extendableMessage.Extensions.Cast<IOpenIdMessageExtension>().Any(ext => ext.TypeUri == typeUri);
foreach (IOpenIdMessageExtension unsignedExtension in this.GetExtensions(extendableMessage, false, isNotSigned)) {
+ Reporting.RecordFeatureUse(unsignedExtension);
unsignedExtension.IsSignedByRemoteParty = false;
extendableMessage.Extensions.Add(unsignedExtension);
}
diff --git a/src/DotNetOpenAuth/OpenId/Interop/OpenIdRelyingPartyShim.cs b/src/DotNetOpenAuth/OpenId/Interop/OpenIdRelyingPartyShim.cs
index 86e80ba..fc0f32e 100644
--- a/src/DotNetOpenAuth/OpenId/Interop/OpenIdRelyingPartyShim.cs
+++ b/src/DotNetOpenAuth/OpenId/Interop/OpenIdRelyingPartyShim.cs
@@ -100,6 +100,13 @@ namespace DotNetOpenAuth.OpenId.Interop {
}
/// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingPartyShim"/> class.
+ /// </summary>
+ public OpenIdRelyingPartyShim() {
+ Reporting.RecordFeatureUse(this);
+ }
+
+ /// <summary>
/// Creates an authentication request to verify that a user controls
/// some given Identifier.
/// </summary>
diff --git a/src/DotNetOpenAuth/OpenId/Provider/AuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/Provider/AuthenticationRequest.cs
index a229488..d22f858 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/AuthenticationRequest.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/AuthenticationRequest.cs
@@ -45,6 +45,8 @@ namespace DotNetOpenAuth.OpenId.Provider {
// If the openid.claimed_id is present, and if it's different than the openid.identity argument, then
// the RP has discovered a claimed identifier that has delegated authentication to this Provider.
this.IsDelegatedIdentifier = this.ClaimedIdentifier != null && this.ClaimedIdentifier != this.LocalIdentifier;
+
+ Reporting.RecordEventOccurrence("AuthenticationRequest.IsDelegatedIdentifier", this.IsDelegatedIdentifier.ToString());
}
#region HostProcessedRequest members
diff --git a/src/DotNetOpenAuth/OpenId/Provider/HostProcessedRequest.cs b/src/DotNetOpenAuth/OpenId/Provider/HostProcessedRequest.cs
index 38d1094..ec0c58a 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/HostProcessedRequest.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/HostProcessedRequest.cs
@@ -39,6 +39,7 @@ namespace DotNetOpenAuth.OpenId.Provider {
Contract.Requires<ArgumentNullException>(provider != null);
this.negativeResponse = new NegativeAssertionResponse(request, provider.Channel);
+ Reporting.RecordEventOccurrence(this, request.Realm);
}
#region IHostProcessedRequest Properties
diff --git a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs
index 3eb24d4..827ca6c 100644
--- a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs
+++ b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs
@@ -84,6 +84,8 @@ namespace DotNetOpenAuth.OpenId.Provider {
}
this.Channel = new OpenIdChannel(this.AssociationStore, nonceStore, this.SecuritySettings);
+
+ Reporting.RecordFeatureAndDependencyUse(this, associationStore, nonceStore);
}
/// <summary>
@@ -401,6 +403,7 @@ namespace DotNetOpenAuth.OpenId.Provider {
}
}
+ Reporting.RecordEventOccurrence(this, "PrepareUnsolicitedAssertion");
return this.Channel.PrepareResponse(positiveAssertion);
}
@@ -513,6 +516,7 @@ namespace DotNetOpenAuth.OpenId.Provider {
private void OnBehaviorsChanged(object sender, NotifyCollectionChangedEventArgs e) {
foreach (IProviderBehavior profile in e.NewItems) {
profile.ApplySecuritySettings(this.SecuritySettings);
+ Reporting.RecordFeatureUse(profile);
}
}
}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs
index f899f03..682e3ff 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/FailedAuthenticationResponse.cs
@@ -31,6 +31,19 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
Contract.Requires<ArgumentNullException>(exception != null);
this.Exception = exception;
+
+ string category = string.Empty;
+ if (Reporting.Enabled) {
+ var pe = exception as ProtocolException;
+ if (pe != null) {
+ var responseMessage = pe.FaultedMessage as IndirectSignedResponse;
+ if (responseMessage != null && responseMessage.ProviderEndpoint != null) { // check "required" parts because this is a failure after all
+ category = responseMessage.ProviderEndpoint.AbsoluteUri;
+ }
+ }
+
+ Reporting.RecordEventOccurrence(this, category);
+ }
}
#region IAuthenticationResponse Members
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs
index 02c5185..869a342 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/NegativeAuthenticationResponse.cs
@@ -30,6 +30,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
internal NegativeAuthenticationResponse(NegativeAssertionResponse response) {
Contract.Requires<ArgumentNullException>(response != null);
this.response = response;
+
+ Reporting.RecordEventOccurrence(this, string.Empty);
}
#region IAuthenticationResponse Properties
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs
index ce77df1..dbf9530 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs
@@ -244,6 +244,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
/// Initializes a new instance of the <see cref="OpenIdMobileTextBox"/> class.
/// </summary>
public OpenIdMobileTextBox() {
+ Reporting.RecordFeatureUse(this);
}
#region Events
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs
index e2d6356..55d6403 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs
@@ -104,6 +104,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
this.channel = new OpenIdChannel(associationStore, nonceStore, this.SecuritySettings);
this.AssociationManager = new AssociationManager(this.Channel, associationStore, this.SecuritySettings);
+
+ Reporting.RecordFeatureAndDependencyUse(this, associationStore, nonceStore);
}
/// <summary>
@@ -593,6 +595,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
private void OnBehaviorsChanged(object sender, NotifyCollectionChangedEventArgs e) {
foreach (IRelyingPartyBehavior profile in e.NewItems) {
profile.ApplySecuritySettings(this.SecuritySettings);
+ Reporting.RecordFeatureUse(profile);
}
}
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs
index d182684..2420fd6 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs
@@ -244,6 +244,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
/// Initializes a new instance of the <see cref="OpenIdRelyingPartyControlBase"/> class.
/// </summary>
protected OpenIdRelyingPartyControlBase() {
+ Reporting.RecordFeatureUse(this);
}
#region Events
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs
index 1c6655d..7a1fbbf 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs
@@ -44,6 +44,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
if (Logger.OpenId.IsInfoEnabled && this.GetType() == typeof(PositiveAnonymousResponse)) {
Logger.OpenId.Info("Received anonymous (identity-less) positive assertion.");
}
+
+ Reporting.RecordEventOccurrence(this, response.ProviderEndpoint.AbsoluteUri);
}
#region IAuthenticationResponse Properties
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorInfoCardButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorInfoCardButton.cs
index 74e37a6..c5dda1c 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorInfoCardButton.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorInfoCardButton.cs
@@ -26,6 +26,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
/// Initializes a new instance of the <see cref="SelectorInfoCardButton"/> class.
/// </summary>
public SelectorInfoCardButton() {
+ Reporting.RecordFeatureUse(this);
}
/// <summary>
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs
index d20bc2b..15b6ca7 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs
@@ -20,6 +20,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
/// Initializes a new instance of the <see cref="SelectorOpenIdButton"/> class.
/// </summary>
public SelectorOpenIdButton() {
+ Reporting.RecordFeatureUse(this);
}
/// <summary>
diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs
index d6d1339..3a05287 100644
--- a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs
+++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs
@@ -21,6 +21,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty {
/// Initializes a new instance of the <see cref="SelectorProviderButton"/> class.
/// </summary>
public SelectorProviderButton() {
+ Reporting.RecordFeatureUse(this);
}
/// <summary>
diff --git a/src/DotNetOpenAuth/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth/Properties/AssemblyInfo.cs
index 51d146c..0bba853 100644
--- a/src/DotNetOpenAuth/Properties/AssemblyInfo.cs
+++ b/src/DotNetOpenAuth/Properties/AssemblyInfo.cs
@@ -83,13 +83,15 @@ using System.Web.UI;
// match the one used by hosting providers. Listing them individually seems to be more common.
[assembly: WebPermission(SecurityAction.RequestMinimum, ConnectPattern = @"http://.*")]
[assembly: WebPermission(SecurityAction.RequestMinimum, ConnectPattern = @"https://.*")]
-
#if PARTIAL_TRUST
// Allows hosting this assembly in an ASP.NET setting. Not all applications
// will host this using ASP.NET, so this is optional. Besides, we need at least
// one optional permission to activate CAS permission shrinking.
[assembly: AspNetHostingPermission(SecurityAction.RequestOptional, Level = AspNetHostingPermissionLevel.Medium)]
+// Allows this assembly to store reporting data.
+[assembly: IsolatedStorageFilePermission(SecurityAction.RequestOptional, UsageAllowed = IsolatedStorageContainment.AssemblyIsolationByUser)]
+
// The following are only required for diagnostic logging (Trace.Write, Debug.Assert, etc.).
#if TRACE || DEBUG
[assembly: KeyContainerPermission(SecurityAction.RequestOptional, Unrestricted = true)]
diff --git a/src/DotNetOpenAuth/Reporting.cs b/src/DotNetOpenAuth/Reporting.cs
new file mode 100644
index 0000000..15af4ba
--- /dev/null
+++ b/src/DotNetOpenAuth/Reporting.cs
@@ -0,0 +1,806 @@
+//-----------------------------------------------------------------------
+// <copyright file="Reporting.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.IO;
+ using System.IO.IsolatedStorage;
+ using System.Linq;
+ using System.Net;
+ using System.Reflection;
+ using System.Security;
+ using System.Text;
+ using System.Threading;
+ using System.Web;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.OAuth;
+ using DotNetOpenAuth.OAuth.ChannelElements;
+
+ /// <summary>
+ /// The statistical reporting mechanism used so this library's project authors
+ /// know what versions and features are in use.
+ /// </summary>
+ internal static class Reporting {
+ /// <summary>
+ /// The isolated storage to use for collecting data in between published reports.
+ /// </summary>
+ private static IsolatedStorageFile file;
+
+ /// <summary>
+ /// The GUID that shows up at the top of all reports from this user/machine/domain.
+ /// </summary>
+ private static Guid reportOriginIdentity;
+
+ /// <summary>
+ /// The recipient of collected reports.
+ /// </summary>
+ private static Uri wellKnownPostLocation = new Uri("https://reports.dotnetopenauth.net/ReportingPost.ashx");
+
+ /// <summary>
+ /// The outgoing HTTP request handler to use for publishing reports.
+ /// </summary>
+ private static IDirectWebRequestHandler webRequestHandler;
+
+ /// <summary>
+ /// A few HTTP request hosts and paths we've seen.
+ /// </summary>
+ private static PersistentHashSet observedRequests;
+
+ /// <summary>
+ /// Cultures that have come in via HTTP requests.
+ /// </summary>
+ private static PersistentHashSet observedCultures;
+
+ /// <summary>
+ /// Features that have been used.
+ /// </summary>
+ private static PersistentHashSet observedFeatures;
+
+ /// <summary>
+ /// A collection of all the observations to include in the report.
+ /// </summary>
+ private static List<PersistentHashSet> observations = new List<PersistentHashSet>();
+
+ /// <summary>
+ /// The named events that we have counters for.
+ /// </summary>
+ private static Dictionary<string, PersistentCounter> events = new Dictionary<string, PersistentCounter>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// The lock acquired while considering whether to publish a report.
+ /// </summary>
+ private static object publishingConsiderationLock = new object();
+
+ /// <summary>
+ /// The time that we last published reports.
+ /// </summary>
+ private static DateTime lastPublished = DateTime.Now;
+
+ /// <summary>
+ /// Initializes static members of the <see cref="Reporting"/> class.
+ /// </summary>
+ static Reporting() {
+ Enabled = DotNetOpenAuthSection.Configuration.Reporting.Enabled;
+ if (Enabled) {
+ try {
+ file = GetIsolatedStorage();
+ reportOriginIdentity = GetOrCreateOriginIdentity();
+
+ webRequestHandler = new StandardWebRequestHandler();
+ observations.Add(observedRequests = new PersistentHashSet(file, "requests.txt", 3));
+ observations.Add(observedCultures = new PersistentHashSet(file, "cultures.txt", 20));
+ observations.Add(observedFeatures = new PersistentHashSet(file, "features.txt", int.MaxValue));
+
+ // Record site-wide features in use.
+ if (HttpContext.Current != null && HttpContext.Current.ApplicationInstance != null) {
+ // MVC or web forms?
+ // front-end or back end web farm?
+ // url rewriting?
+ ////RecordFeatureUse(IsMVC ? "ASP.NET MVC" : "ASP.NET Web Forms");
+ }
+ } catch (Exception e) {
+ // This is supposed to be as low-risk as possible, so if it fails, just disable reporting
+ // and avoid rethrowing.
+ Enabled = false;
+ Logger.Library.Error("Error while trying to initialize reporting.", e);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this reporting is enabled.
+ /// </summary>
+ /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value>
+ internal static bool Enabled { get; set; }
+
+ /// <summary>
+ /// Gets the configuration to use for reporting.
+ /// </summary>
+ private static ReportingElement Configuration {
+ get { return DotNetOpenAuthSection.Configuration.Reporting; }
+ }
+
+ /// <summary>
+ /// Records an event occurrence.
+ /// </summary>
+ /// <param name="eventName">Name of the event.</param>
+ /// <param name="category">The category within the event. Null and empty strings are allowed, but considered the same.</param>
+ internal static void RecordEventOccurrence(string eventName, string category) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(eventName));
+
+ if (Enabled && Configuration.IncludeEventStatistics) {
+ PersistentCounter counter;
+ lock (events) {
+ if (!events.TryGetValue(eventName, out counter)) {
+ events[eventName] = counter = new PersistentCounter(file, "event-" + SanitizeFileName(eventName) + ".txt");
+ }
+ }
+
+ counter.Increment(category);
+ Touch();
+ }
+ }
+
+ /// <summary>
+ /// Records an event occurence.
+ /// </summary>
+ /// <param name="eventNameByObjectType">The object whose type name is the event name to record.</param>
+ /// <param name="category">The category within the event. Null and empty strings are allowed, but considered the same.</param>
+ internal static void RecordEventOccurrence(object eventNameByObjectType, string category) {
+ Contract.Requires<ArgumentNullException>(eventNameByObjectType != null);
+
+ if (Enabled && Configuration.IncludeEventStatistics) {
+ RecordEventOccurrence(eventNameByObjectType.GetType().Name, category);
+ }
+ }
+
+ /// <summary>
+ /// Records the use of a feature by name.
+ /// </summary>
+ /// <param name="feature">The feature.</param>
+ internal static void RecordFeatureUse(string feature) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(feature));
+
+ if (Enabled && Configuration.IncludeFeatureUsage) {
+ observedFeatures.Add(feature);
+ Touch();
+ }
+ }
+
+ /// <summary>
+ /// Records the use of a feature by object type.
+ /// </summary>
+ /// <param name="value">The object whose type is the feature to set as used.</param>
+ internal static void RecordFeatureUse(object value) {
+ Contract.Requires<ArgumentNullException>(value != null);
+
+ if (Enabled && Configuration.IncludeFeatureUsage) {
+ observedFeatures.Add(value.GetType().Name);
+ Touch();
+ }
+ }
+
+ /// <summary>
+ /// Records the use of a feature by object type.
+ /// </summary>
+ /// <param name="value">The object whose type is the feature to set as used.</param>
+ /// <param name="dependency1">Some dependency used by <paramref name="value"/>.</param>
+ /// <param name="dependency2">Some dependency used by <paramref name="value"/>.</param>
+ internal static void RecordFeatureAndDependencyUse(object value, object dependency1, object dependency2) {
+ Contract.Requires<ArgumentNullException>(value != null);
+
+ if (Enabled && Configuration.IncludeFeatureUsage) {
+ StringBuilder builder = new StringBuilder();
+ builder.Append(value.GetType().Name);
+ builder.Append(" ");
+ builder.Append(dependency1 != null ? dependency1.GetType().Name : "(null)");
+ builder.Append(" ");
+ builder.Append(dependency2 != null ? dependency2.GetType().Name : "(null)");
+ observedFeatures.Add(builder.ToString());
+ Touch();
+ }
+ }
+
+ /// <summary>
+ /// Records the feature and dependency use.
+ /// </summary>
+ /// <param name="value">The consumer or service provider.</param>
+ /// <param name="service">The service.</param>
+ /// <param name="tokenManager">The token manager.</param>
+ /// <param name="nonceStore">The nonce store.</param>
+ internal static void RecordFeatureAndDependencyUse(object value, ServiceProviderDescription service, ITokenManager tokenManager, INonceStore nonceStore) {
+ Contract.Requires<ArgumentNullException>(value != null);
+ Contract.Requires<ArgumentNullException>(service != null);
+ Contract.Requires<ArgumentNullException>(tokenManager != null);
+
+ if (Enabled && Configuration.IncludeFeatureUsage) {
+ StringBuilder builder = new StringBuilder();
+ builder.Append(value.GetType().Name);
+ builder.Append(" ");
+ builder.Append(tokenManager.GetType().Name);
+ if (nonceStore != null) {
+ builder.Append(" ");
+ builder.Append(nonceStore.GetType().Name);
+ }
+ builder.Append(" ");
+ builder.Append(service.Version);
+ builder.Append(" ");
+ builder.Append(service.UserAuthorizationEndpoint);
+ observedFeatures.Add(builder.ToString());
+ Touch();
+ }
+ }
+
+ /// <summary>
+ /// Records statistics collected from incoming requests.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ internal static void RecordRequestStatistics(HttpRequestInfo request) {
+ Contract.Requires<ArgumentNullException>(request != null);
+
+ if (Enabled) {
+ if (Configuration.IncludeCultures) {
+ observedCultures.Add(Thread.CurrentThread.CurrentCulture.Name);
+ }
+
+ if (Configuration.IncludeLocalRequestUris && !observedRequests.IsFull) {
+ var requestBuilder = new UriBuilder(request.UrlBeforeRewriting);
+ requestBuilder.Query = null;
+ requestBuilder.Fragment = null;
+ observedRequests.Add(requestBuilder.Uri.AbsoluteUri);
+ }
+
+ Touch();
+ }
+ }
+
+ /// <summary>
+ /// Assembles a report for submission.
+ /// </summary>
+ /// <returns>A stream that contains the report.</returns>
+ private static Stream GetReport() {
+ var stream = new MemoryStream();
+ var writer = new StreamWriter(stream, Encoding.UTF8);
+ writer.WriteLine(reportOriginIdentity.ToString("B"));
+ writer.WriteLine(Util.LibraryVersion);
+ writer.WriteLine(".NET Framework {0}", Environment.Version);
+
+ foreach (var observation in observations) {
+ observation.Flush();
+ writer.WriteLine("====================================");
+ writer.WriteLine(observation.FileName);
+ try {
+ using (var fileStream = new IsolatedStorageFileStream(observation.FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) {
+ writer.Flush();
+ fileStream.CopyTo(writer.BaseStream);
+ }
+ } catch (FileNotFoundException) {
+ writer.WriteLine("(missing)");
+ }
+ }
+
+ // Not all event counters may have even loaded in this app instance.
+ // We flush the ones in memory, and then read all of them off disk.
+ foreach (var counter in events.Values) {
+ counter.Flush();
+ }
+
+ foreach (string eventFile in file.GetFileNames("event-*.txt")) {
+ writer.WriteLine("====================================");
+ writer.WriteLine(eventFile);
+ using (var fileStream = new IsolatedStorageFileStream(eventFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) {
+ writer.Flush();
+ fileStream.CopyTo(writer.BaseStream);
+ }
+ }
+
+ // Make sure the stream is positioned at the beginning.
+ writer.Flush();
+ stream.Position = 0;
+ return stream;
+ }
+
+ /// <summary>
+ /// Sends the usage reports to the library authors.
+ /// </summary>
+ /// <returns>A value indicating whether submitting the report was successful.</returns>
+ private static bool SendStats() {
+ try {
+ var request = (HttpWebRequest)WebRequest.Create(wellKnownPostLocation);
+ request.UserAgent = Util.LibraryVersion;
+ request.AllowAutoRedirect = false;
+ request.Method = "POST";
+ request.ContentType = "text/dnoa-report1";
+ Stream report = GetReport();
+ request.ContentLength = report.Length;
+ using (var requestStream = webRequestHandler.GetRequestStream(request)) {
+ report.CopyTo(requestStream);
+ }
+
+ using (var response = webRequestHandler.GetResponse(request)) {
+ Logger.Library.Info("Statistical report submitted successfully.");
+
+ // The response stream may contain a message for the webmaster.
+ // Since as part of the report we submit the library version number,
+ // the report receiving service may have alerts such as:
+ // "You're using an obsolete version with exploitable security vulnerabilities."
+ using (var responseReader = response.GetResponseReader()) {
+ string line = responseReader.ReadLine();
+ if (line != null) {
+ DemuxLogMessage(line);
+ }
+ }
+ }
+
+ // Report submission was successful. Reset all counters.
+ lock (events) {
+ foreach (PersistentCounter counter in events.Values) {
+ counter.Reset();
+ counter.Flush();
+ }
+
+ // We can just delete the files for counters that are not currently loaded.
+ foreach (string eventFile in file.GetFileNames("event-*.txt")) {
+ if (!events.Values.Any(e => string.Equals(e.FileName, eventFile, StringComparison.OrdinalIgnoreCase))) {
+ file.DeleteFile(eventFile);
+ }
+ }
+ }
+
+ return true;
+ } catch (ProtocolException ex) {
+ Logger.Library.Error("Unable to submit statistical report due to an HTTP error.", ex);
+ } catch (FileNotFoundException ex) {
+ Logger.Library.Error("Unable to submit statistical report because the report file is missing.", ex);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Interprets the reporting response as a log message if possible.
+ /// </summary>
+ /// <param name="line">The line from the HTTP response to interpret as a log message.</param>
+ private static void DemuxLogMessage(string line) {
+ if (line != null) {
+ string[] parts = line.Split(new char[] { ' ' }, 2);
+ if (parts.Length == 2) {
+ string level = parts[0];
+ string message = parts[1];
+ switch (level) {
+ case "INFO":
+ Logger.Library.Info(message);
+ break;
+ case "WARN":
+ Logger.Library.Warn(message);
+ break;
+ case "ERROR":
+ Logger.Library.Error(message);
+ break;
+ case "FATAL":
+ Logger.Library.Fatal(message);
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Called by every internal/public method on this class to give
+ /// periodic operations a chance to run.
+ /// </summary>
+ private static void Touch() {
+ // Publish stats if it's time to do so.
+ lock (publishingConsiderationLock) {
+ if (DateTime.Now - lastPublished > Configuration.MinimumReportingInterval) {
+ lastPublished = DateTime.Now;
+ SendStatsAsync();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Sends the stats report asynchronously, and careful to not throw any unhandled exceptions.
+ /// </summary>
+ private static void SendStatsAsync() {
+ // Do it on a background thread since it could take a while and we
+ // don't want to slow down this request we're borrowing.
+ ThreadPool.QueueUserWorkItem(state => {
+ try {
+ SendStats();
+ } catch (Exception ex) {
+ // Something bad and unexpected happened. Just deactivate to avoid more trouble.
+ Logger.Library.Error("Error while trying to submit statistical report.", ex);
+ Enabled = false;
+ }
+ });
+ }
+
+ /// <summary>
+ /// Gets the isolated storage to use for reporting.
+ /// </summary>
+ /// <returns>An isolated storage location appropriate for our host.</returns>
+ private static IsolatedStorageFile GetIsolatedStorage() {
+ Contract.Ensures(Contract.Result<IsolatedStorageFile>() != null);
+
+ IsolatedStorageFile result = null;
+
+ // We'll try for whatever storage location we can get,
+ // and not catch exceptions from the last attempt so that
+ // the overall failure is caught by our caller.
+ try {
+ // This works on Personal Web Server
+ result = IsolatedStorageFile.GetUserStoreForDomain();
+ } catch (SecurityException) {
+ } catch (IsolatedStorageException) {
+ }
+
+ // This works on IIS when full trust is granted.
+ if (result == null) {
+ result = IsolatedStorageFile.GetMachineStoreForDomain();
+ }
+
+ Logger.Library.InfoFormat("Reporting will use isolated storage with scope: {0}", result.Scope);
+ return result;
+ }
+
+ /// <summary>
+ /// Gets a unique, pseudonymous identifier for this particular web site or application.
+ /// </summary>
+ /// <returns>A GUID that will serve as the identifier.</returns>
+ /// <remarks>
+ /// The identifier is made persistent by storing the identifier in isolated storage.
+ /// If an existing identifier is not found, a new one is created, persisted, and returned.
+ /// </remarks>
+ private static Guid GetOrCreateOriginIdentity() {
+ Contract.Requires<InvalidOperationException>(file != null);
+ Contract.Ensures(Contract.Result<Guid>() != Guid.Empty);
+
+ Guid identityGuid = Guid.Empty;
+ const int GuidLength = 16;
+ using (var identityFileStream = new IsolatedStorageFileStream("identity.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, file)) {
+ if (identityFileStream.Length == GuidLength) {
+ byte[] guidBytes = new byte[GuidLength];
+ if (identityFileStream.Read(guidBytes, 0, GuidLength) == GuidLength) {
+ identityGuid = new Guid(guidBytes);
+ }
+ }
+
+ if (identityGuid == Guid.Empty) {
+ identityGuid = Guid.NewGuid();
+ byte[] guidBytes = identityGuid.ToByteArray();
+ identityFileStream.SetLength(0);
+ identityFileStream.Write(guidBytes, 0, guidBytes.Length);
+ }
+
+ return identityGuid;
+ }
+ }
+
+ /// <summary>
+ /// Sanitizes the name of the file so it only includes valid filename characters.
+ /// </summary>
+ /// <param name="fileName">The filename to sanitize.</param>
+ /// <returns>The filename, with any and all invalid filename characters replaced with the hyphen (-) character.</returns>
+ private static string SanitizeFileName(string fileName) {
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(fileName));
+ char[] invalidCharacters = Path.GetInvalidFileNameChars();
+ if (fileName.IndexOfAny(invalidCharacters) < 0) {
+ return fileName; // nothing invalid about this filename.
+ }
+
+ // Use a stringbuilder since we may be replacing several characters
+ // and we don't want to instantiate a new string buffer for each new version.
+ StringBuilder sanitized = new StringBuilder(fileName);
+ foreach (char invalidChar in invalidCharacters) {
+ sanitized.Replace(invalidChar, '-');
+ }
+
+ return sanitized.ToString();
+ }
+
+ /// <summary>
+ /// A set of values that persist the set to disk.
+ /// </summary>
+ private class PersistentHashSet : IDisposable {
+ /// <summary>
+ /// The isolated persistent storage.
+ /// </summary>
+ private readonly FileStream fileStream;
+
+ /// <summary>
+ /// The persistent reader.
+ /// </summary>
+ private readonly StreamReader reader;
+
+ /// <summary>
+ /// The persistent writer.
+ /// </summary>
+ private readonly StreamWriter writer;
+
+ /// <summary>
+ /// The total set of elements.
+ /// </summary>
+ private readonly HashSet<string> memorySet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// The maximum number of elements to track before not storing new elements.
+ /// </summary>
+ private readonly int maximumElements;
+
+ /// <summary>
+ /// The set of new elements added to the <see cref="memorySet"/> since the last flush.
+ /// </summary>
+ private List<string> newElements = new List<string>();
+
+ /// <summary>
+ /// The time the last flush occurred.
+ /// </summary>
+ private DateTime lastFlushed;
+
+ /// <summary>
+ /// A flag indicating whether the set has changed since it was last flushed.
+ /// </summary>
+ private bool dirty;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PersistentHashSet"/> class.
+ /// </summary>
+ /// <param name="storage">The storage location.</param>
+ /// <param name="fileName">Name of the file.</param>
+ /// <param name="maximumElements">The maximum number of elements to track.</param>
+ internal PersistentHashSet(IsolatedStorageFile storage, string fileName, int maximumElements) {
+ Contract.Requires<ArgumentNullException>(storage != null);
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(fileName));
+ this.FileName = fileName;
+ this.maximumElements = maximumElements;
+
+ // Load the file into memory.
+ this.fileStream = new IsolatedStorageFileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, storage);
+ this.reader = new StreamReader(this.fileStream, Encoding.UTF8);
+ while (!this.reader.EndOfStream) {
+ this.memorySet.Add(this.reader.ReadLine());
+ }
+
+ this.writer = new StreamWriter(this.fileStream, Encoding.UTF8);
+ this.lastFlushed = DateTime.Now;
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether the hashset has reached capacity and is not storing more elements.
+ /// </summary>
+ /// <value><c>true</c> if this instance is full; otherwise, <c>false</c>.</value>
+ internal bool IsFull {
+ get {
+ lock (this.memorySet) {
+ return this.memorySet.Count >= this.maximumElements;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the name of the file.
+ /// </summary>
+ /// <value>The name of the file.</value>
+ internal string FileName { get; private set; }
+
+ #region IDisposable Members
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose() {
+ this.Dispose(true);
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Adds a value to the set.
+ /// </summary>
+ /// <param name="value">The value.</param>
+ internal void Add(string value) {
+ lock (this.memorySet) {
+ if (!this.IsFull) {
+ if (this.memorySet.Add(value)) {
+ this.newElements.Add(value);
+ this.dirty = true;
+
+ if (this.IsFull) {
+ this.Flush();
+ }
+ }
+
+ if (this.dirty && DateTime.Now - this.lastFlushed > Configuration.MinimumFlushInterval) {
+ this.Flush();
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Flushes any newly added values to disk.
+ /// </summary>
+ internal void Flush() {
+ lock (this.memorySet) {
+ foreach (string element in this.newElements) {
+ this.writer.WriteLine(element);
+ }
+ this.writer.Flush();
+
+ // Assign a whole new list since future lists might be smaller in order to
+ // decrease demand on memory.
+ this.newElements = new List<string>();
+ this.dirty = false;
+ this.lastFlushed = DateTime.Now;
+ }
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing) {
+ if (disposing) {
+ this.writer.Dispose();
+ this.reader.Dispose();
+ this.fileStream.Dispose();
+ }
+ }
+ }
+
+ /// <summary>
+ /// A feature usage counter.
+ /// </summary>
+ private class PersistentCounter : IDisposable {
+ /// <summary>
+ /// The separator to use between category names and their individual counters.
+ /// </summary>
+ private static readonly char[] separator = new char[] { '\t' };
+
+ /// <summary>
+ /// The isolated persistent storage.
+ /// </summary>
+ private readonly FileStream fileStream;
+
+ /// <summary>
+ /// The persistent reader.
+ /// </summary>
+ private readonly StreamReader reader;
+
+ /// <summary>
+ /// The persistent writer.
+ /// </summary>
+ private readonly StreamWriter writer;
+
+ /// <summary>
+ /// The time the last flush occurred.
+ /// </summary>
+ private DateTime lastFlushed;
+
+ /// <summary>
+ /// The in-memory copy of the counter.
+ /// </summary>
+ private Dictionary<string, int> counters = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// A flag indicating whether the set has changed since it was last flushed.
+ /// </summary>
+ private bool dirty;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PersistentCounter"/> class.
+ /// </summary>
+ /// <param name="storage">The storage location.</param>
+ /// <param name="fileName">Name of the file.</param>
+ internal PersistentCounter(IsolatedStorageFile storage, string fileName) {
+ Contract.Requires<ArgumentNullException>(storage != null);
+ Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(fileName));
+ this.FileName = fileName;
+
+ // Load the file into memory.
+ this.fileStream = new IsolatedStorageFileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, storage);
+ this.reader = new StreamReader(this.fileStream, Encoding.UTF8);
+ while (!this.reader.EndOfStream) {
+ string line = this.reader.ReadLine();
+ string[] parts = line.Split(separator, 2);
+ int counter;
+ if (int.TryParse(parts[0], out counter)) {
+ string category = string.Empty;
+ if (parts.Length > 1) {
+ category = parts[1];
+ }
+ this.counters[category] = counter;
+ }
+ }
+
+ this.writer = new StreamWriter(this.fileStream, Encoding.UTF8);
+ this.lastFlushed = DateTime.Now;
+ }
+
+ /// <summary>
+ /// Gets the name of the file.
+ /// </summary>
+ /// <value>The name of the file.</value>
+ internal string FileName { get; private set; }
+
+ #region IDisposable Members
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose() {
+ this.Dispose(true);
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Increments the counter.
+ /// </summary>
+ /// <param name="category">The category within the event. Null and empty strings are allowed, but considered the same.</param>
+ internal void Increment(string category) {
+ if (category == null) {
+ category = string.Empty;
+ }
+ lock (this) {
+ int counter;
+ this.counters.TryGetValue(category, out counter);
+ this.counters[category] = counter + 1;
+ this.dirty = true;
+ if (this.dirty && DateTime.Now - this.lastFlushed > Configuration.MinimumFlushInterval) {
+ this.Flush();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Flushes any newly added values to disk.
+ /// </summary>
+ internal void Flush() {
+ lock (this) {
+ this.writer.BaseStream.Position = 0;
+ this.writer.BaseStream.SetLength(0); // truncate file
+ foreach (var pair in this.counters) {
+ this.writer.Write(pair.Value);
+ this.writer.Write(separator[0]);
+ this.writer.WriteLine(pair.Key);
+ }
+ this.writer.Flush();
+ this.dirty = false;
+ this.lastFlushed = DateTime.Now;
+ }
+ }
+
+ /// <summary>
+ /// Resets all counters.
+ /// </summary>
+ internal void Reset() {
+ lock (this) {
+ this.counters.Clear();
+ }
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing) {
+ if (disposing) {
+ this.writer.Dispose();
+ this.reader.Dispose();
+ this.fileStream.Dispose();
+ }
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth/XrdsPublisher.cs b/src/DotNetOpenAuth/XrdsPublisher.cs
index 332e3fa..e7c04d8 100644
--- a/src/DotNetOpenAuth/XrdsPublisher.cs
+++ b/src/DotNetOpenAuth/XrdsPublisher.cs
@@ -95,6 +95,13 @@ namespace DotNetOpenAuth {
/// </summary>
private const string EnabledViewStateKey = "Enabled";
+ /// <summary>
+ /// Initializes a new instance of the <see cref="XrdsPublisher"/> class.
+ /// </summary>
+ public XrdsPublisher() {
+ Reporting.RecordFeatureUse(this);
+ }
+
#region Properties
/// <summary>
diff --git a/src/DotNetOpenAuth/Yadis/Yadis.cs b/src/DotNetOpenAuth/Yadis/Yadis.cs
index a9a573b..589a155 100644
--- a/src/DotNetOpenAuth/Yadis/Yadis.cs
+++ b/src/DotNetOpenAuth/Yadis/Yadis.cs
@@ -73,6 +73,7 @@ namespace DotNetOpenAuth.Yadis {
CachedDirectWebResponse response2 = null;
if (IsXrdsDocument(response)) {
Logger.Yadis.Debug("An XRDS response was received from GET at user-supplied identifier.");
+ Reporting.RecordEventOccurrence("Yadis", "XRDS in initial response");
response2 = response;
} else {
string uriString = response.Headers.Get(HeaderName);
@@ -80,12 +81,14 @@ namespace DotNetOpenAuth.Yadis {
if (uriString != null) {
if (Uri.TryCreate(uriString, UriKind.Absolute, out url)) {
Logger.Yadis.DebugFormat("{0} found in HTTP header. Preparing to pull XRDS from {1}", HeaderName, url);
+ Reporting.RecordEventOccurrence("Yadis", "XRDS referenced in HTTP header");
}
}
if (url == null && response.ContentType != null && (response.ContentType.MediaType == ContentTypes.Html || response.ContentType.MediaType == ContentTypes.XHtml)) {
url = FindYadisDocumentLocationInHtmlMetaTags(response.GetResponseString());
if (url != null) {
Logger.Yadis.DebugFormat("{0} found in HTML Http-Equiv tag. Preparing to pull XRDS from {1}", HeaderName, url);
+ Reporting.RecordEventOccurrence("Yadis", "XRDS referenced in HTML");
}
}
if (url != null) {