This repository has been archived by the owner. It is now read-only.
Permalink
Cannot retrieve contributors at this time
using System; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Net; | |
using System.Reflection; | |
using System.Threading; | |
using System.Web; | |
using OfficeDevPnP.Core; | |
using OfficeDevPnP.Core.Diagnostics; | |
using OfficeDevPnP.Core.Utilities; | |
using System.Configuration; | |
using System.Threading.Tasks; | |
using System.Net.Http; | |
using Newtonsoft.Json; | |
using OfficeDevPnP.Core.Utilities.Async; | |
using System.IdentityModel.Tokens.Jwt; | |
using System.Collections.Generic; | |
using OfficeDevPnP.Core.Utilities.Context; | |
using OfficeDevPnP.Core.Framework.Provisioning.ObjectHandlers; | |
#if !SP2013 && !SP2016 | |
using OfficeDevPnP.Core.Sites; | |
#endif | |
namespace Microsoft.SharePoint.Client | |
{ | |
/// <summary> | |
/// Class that deals with cloning client context object, getting access token and validates server version | |
/// </summary> | |
public static partial class ClientContextExtensions | |
{ | |
private static string userAgentFromConfig = null; | |
private static string accessToken = null; | |
private static bool hasAuthCookies; | |
/// <summary> | |
/// Static constructor, only executed once per class load | |
/// </summary> | |
static ClientContextExtensions() | |
{ | |
ClientContextExtensions.userAgentFromConfig = ConfigurationManager.AppSettings["SharePointPnPUserAgent"]; | |
if (string.IsNullOrWhiteSpace(ClientContextExtensions.userAgentFromConfig)) | |
{ | |
ClientContextExtensions.userAgentFromConfig = System.Environment.GetEnvironmentVariable("SharePointPnPUserAgent", EnvironmentVariableTarget.Process); | |
} | |
} | |
#if ONPREMISES | |
private const string MicrosoftSharePointTeamServicesHeader = "MicrosoftSharePointTeamServices"; | |
#endif | |
/// <summary> | |
/// Clones a ClientContext object while "taking over" the security context of the existing ClientContext instance | |
/// </summary> | |
/// <param name="clientContext">ClientContext to be cloned</param> | |
/// <param name="siteUrl">Site URL to be used for cloned ClientContext</param> | |
/// <param name="accessTokens">Dictionary of access tokens for sites URLs</param> | |
/// <returns>A ClientContext object created for the passed site URL</returns> | |
public static ClientContext Clone(this ClientRuntimeContext clientContext, string siteUrl, Dictionary<String, String> accessTokens = null) | |
{ | |
if (string.IsNullOrWhiteSpace(siteUrl)) | |
{ | |
throw new ArgumentException(CoreResources.ClientContextExtensions_Clone_Url_of_the_site_is_required_, nameof(siteUrl)); | |
} | |
return clientContext.Clone(new Uri(siteUrl), accessTokens); | |
} | |
#if !ONPREMISES | |
/// <summary> | |
/// Executes the current set of data retrieval queries and method invocations and retries it if needed using the Task Library. | |
/// </summary> | |
/// <param name="clientContext">clientContext to operate on</param> | |
/// <param name="retryCount">Number of times to retry the request</param> | |
/// <param name="delay">Milliseconds to wait before retrying the request. The delay will be increased (doubled) every retry</param> | |
/// <param name="userAgent">UserAgent string value to insert for this request. You can define this value in your app's config file using key="SharePointPnPUserAgent" value="PnPRocks"></param> | |
public static Task ExecuteQueryRetryAsync(this ClientRuntimeContext clientContext, int retryCount = 10, int delay = 500, string userAgent = null) | |
{ | |
return ExecuteQueryImplementation(clientContext, retryCount, delay, userAgent); | |
} | |
#endif | |
/// <summary> | |
/// Executes the current set of data retrieval queries and method invocations and retries it if needed. | |
/// </summary> | |
/// <param name="clientContext">clientContext to operate on</param> | |
/// <param name="retryCount">Number of times to retry the request</param> | |
/// <param name="delay">Milliseconds to wait before retrying the request. The delay will be increased (doubled) every retry</param> | |
/// <param name="userAgent">UserAgent string value to insert for this request. You can define this value in your app's config file using key="SharePointPnPUserAgent" value="PnPRocks"></param> | |
public static void ExecuteQueryRetry(this ClientRuntimeContext clientContext, int retryCount = 10, int delay = 500, string userAgent = null) | |
{ | |
#if !ONPREMISES | |
Task.Run(() => ExecuteQueryImplementation(clientContext, retryCount, delay, userAgent)).GetAwaiter().GetResult(); | |
#else | |
ExecuteQueryImplementation(clientContext, retryCount, delay, userAgent); | |
#endif | |
} | |
#if !ONPREMISES | |
private static async Task ExecuteQueryImplementation(ClientRuntimeContext clientContext, int retryCount = 10, int delay = 500, string userAgent = null) | |
#else | |
private static void ExecuteQueryImplementation(ClientRuntimeContext clientContext, int retryCount = 10, int delay = 500, string userAgent = null) | |
#endif | |
{ | |
#if !ONPREMISES | |
await new SynchronizationContextRemover(); | |
// Set the TLS preference. Needed on some server os's to work when Office 365 removes support for TLS 1.0 | |
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; | |
#endif | |
var clientTag = string.Empty; | |
if (clientContext is PnPClientContext) | |
{ | |
retryCount = (clientContext as PnPClientContext).RetryCount; | |
delay = (clientContext as PnPClientContext).Delay; | |
clientTag = (clientContext as PnPClientContext).ClientTag; | |
} | |
int retryAttempts = 0; | |
int backoffInterval = delay; | |
#if !ONPREMISES | |
int retryAfterInterval = 0; | |
bool retry = false; | |
ClientRequestWrapper wrapper = null; | |
#endif | |
if (retryCount <= 0) | |
throw new ArgumentException("Provide a retry count greater than zero."); | |
if (delay <= 0) | |
throw new ArgumentException("Provide a delay greater than zero."); | |
// Do while retry attempt is less than retry count | |
while (retryAttempts < retryCount) | |
{ | |
try | |
{ | |
clientContext.ClientTag = SetClientTag(clientTag); | |
// Make CSOM request more reliable by disabling the return value cache. Given we | |
// often clone context objects and the default value is | |
#if !SP2013 | |
clientContext.DisableReturnValueCache = true; | |
#endif | |
// Add event handler to "insert" app decoration header to mark the PnP Sites Core library as a known application | |
EventHandler<WebRequestEventArgs> appDecorationHandler = AttachRequestUserAgent(userAgent); | |
clientContext.ExecutingWebRequest += appDecorationHandler; | |
// DO NOT CHANGE THIS TO EXECUTEQUERYRETRY | |
#if !ONPREMISES | |
if (!retry) | |
{ | |
await clientContext.ExecuteQueryAsync(); | |
} | |
else | |
{ | |
if (wrapper != null && wrapper.Value != null) | |
{ | |
await clientContext.RetryQueryAsync(wrapper.Value); | |
} | |
} | |
#else | |
clientContext.ExecuteQuery(); | |
#endif | |
// Remove the app decoration event handler after the executequery | |
clientContext.ExecutingWebRequest -= appDecorationHandler; | |
return; | |
} | |
catch (WebException wex) | |
{ | |
var response = wex.Response as HttpWebResponse; | |
// Check if request was throttled - http status code 429 | |
// Check is request failed due to server unavailable - http status code 503 | |
if (response != null && | |
(response.StatusCode == (HttpStatusCode)429 | |
|| response.StatusCode == (HttpStatusCode)503 | |
// || response.StatusCode == (HttpStatusCode)500 | |
)) | |
{ | |
Log.Warning(Constants.LOGGING_SOURCE, CoreResources.ClientContextExtensions_ExecuteQueryRetry, backoffInterval); | |
#if !ONPREMISES | |
wrapper = (ClientRequestWrapper)wex.Data["ClientRequest"]; | |
retry = true; | |
// Determine the retry after value - use the retry-after header when available | |
// Retry-After seems to default to a fixed 120 seconds in most cases, let's revert to our | |
// previous logic | |
//string retryAfterHeader = response.GetResponseHeader("Retry-After"); | |
//if (!string.IsNullOrEmpty(retryAfterHeader)) | |
//{ | |
// if (!Int32.TryParse(retryAfterHeader, out retryAfterInterval)) | |
// { | |
// retryAfterInterval = backoffInterval; | |
// } | |
//} | |
//else | |
//{ | |
retryAfterInterval = backoffInterval; | |
//} | |
//Add delay for retry, retry-after header is specified in seconds | |
await Task.Delay(retryAfterInterval); | |
#else | |
Thread.Sleep(backoffInterval); | |
#endif | |
//Add to retry count and increase delay. | |
retryAttempts++; | |
backoffInterval = backoffInterval * 2; | |
} | |
else | |
{ | |
var errorSb = new System.Text.StringBuilder(); | |
errorSb.AppendLine(wex.ToString()); | |
if (response != null) | |
{ | |
//if(response.Headers["SPRequestGuid"] != null) | |
if (response.Headers.AllKeys.Any(k => string.Equals(k, "SPRequestGuid", StringComparison.InvariantCultureIgnoreCase))) | |
{ | |
var spRequestGuid = response.Headers["SPRequestGuid"]; | |
errorSb.AppendLine($"ServerErrorTraceCorrelationId: {spRequestGuid}"); | |
} | |
} | |
Log.Error(Constants.LOGGING_SOURCE, CoreResources.ClientContextExtensions_ExecuteQueryRetryException, errorSb.ToString()); | |
throw; | |
} | |
} | |
catch (Microsoft.SharePoint.Client.ServerException serverEx) | |
{ | |
var errorSb = new System.Text.StringBuilder(); | |
errorSb.AppendLine(serverEx.ToString()); | |
errorSb.AppendLine($"ServerErrorCode: {serverEx.ServerErrorCode}"); | |
errorSb.AppendLine($"ServerErrorTypeName: {serverEx.ServerErrorTypeName}"); | |
errorSb.AppendLine($"ServerErrorTraceCorrelationId: {serverEx.ServerErrorTraceCorrelationId}"); | |
errorSb.AppendLine($"ServerErrorValue: {serverEx.ServerErrorValue}"); | |
errorSb.AppendLine($"ServerErrorDetails: {serverEx.ServerErrorDetails}"); | |
Log.Error(Constants.LOGGING_SOURCE, CoreResources.ClientContextExtensions_ExecuteQueryRetryException, errorSb.ToString()); | |
throw; | |
} | |
} | |
throw new MaximumRetryAttemptedException($"Maximum retry attempts {retryCount}, has be attempted."); | |
} | |
/// <summary> | |
/// Attaches either a passed user agent, or one defined in the App.config file, to the WebRequstExecutor UserAgent property. | |
/// </summary> | |
/// <param name="customUserAgent">a custom user agent to override any defined in App.config</param> | |
/// <returns>An EventHandler of WebRequestEventArgs.</returns> | |
private static EventHandler<WebRequestEventArgs> AttachRequestUserAgent(string customUserAgent) | |
{ | |
return (s, e) => | |
{ | |
bool overrideUserAgent = true; | |
var existingUserAgent = e.WebRequestExecutor.WebRequest.UserAgent; | |
if (!string.IsNullOrEmpty(existingUserAgent) && existingUserAgent.StartsWith("NONISV|SharePointPnP|PnPPS/")) | |
{ | |
overrideUserAgent = false; | |
} | |
if (overrideUserAgent) | |
{ | |
if (string.IsNullOrEmpty(customUserAgent) && !string.IsNullOrEmpty(ClientContextExtensions.userAgentFromConfig)) | |
{ | |
customUserAgent = userAgentFromConfig; | |
} | |
e.WebRequestExecutor.WebRequest.UserAgent = string.IsNullOrEmpty(customUserAgent) ? $"{PnPCoreUtilities.PnPCoreUserAgent}" : customUserAgent; | |
} | |
}; | |
} | |
/// <summary> | |
/// Sets the client context client tag on outgoing CSOM requests. | |
/// </summary> | |
/// <param name="clientTag">An optional client tag to set on client context requests.</param> | |
/// <returns></returns> | |
private static string SetClientTag(string clientTag = "") | |
{ | |
// ClientTag property is limited to 32 chars | |
if (string.IsNullOrEmpty(clientTag)) | |
{ | |
clientTag = $"{PnPCoreUtilities.PnPCoreVersionTag}:{GetCallingPnPMethod()}"; | |
} | |
if (clientTag.Length > 32) | |
{ | |
clientTag = clientTag.Substring(0, 32); | |
} | |
return clientTag; | |
} | |
/// <summary> | |
/// Clones a ClientContext object while "taking over" the security context of the existing ClientContext instance | |
/// </summary> | |
/// <param name="clientContext">ClientContext to be cloned</param> | |
/// <param name="siteUrl">Site URL to be used for cloned ClientContext</param> | |
/// <param name="accessTokens">Dictionary of access tokens for sites URLs</param> | |
/// <returns>A ClientContext object created for the passed site URL</returns> | |
public static ClientContext Clone(this ClientRuntimeContext clientContext, Uri siteUrl, Dictionary<String, String> accessTokens = null) | |
{ | |
return Clone(clientContext, new ClientContext(siteUrl), siteUrl, accessTokens); | |
} | |
/// <summary> | |
/// Clones a ClientContext object while "taking over" the security context of the existing ClientContext instance | |
/// </summary> | |
/// <param name="clientContext">ClientContext to be cloned</param> | |
/// <param name="targetContext">CientContext stub to be used for cloning</param> | |
/// <param name="siteUrl">Site URL to be used for cloned ClientContext</param> | |
/// <param name="accessTokens">Dictionary of access tokens for sites URLs</param> | |
/// <returns>A ClientContext object created for the passed site URL</returns> | |
internal static ClientContext Clone(this ClientRuntimeContext clientContext, ClientContext targetContext, Uri siteUrl, Dictionary<String, String> accessTokens = null) | |
{ | |
if (siteUrl == null) | |
{ | |
throw new ArgumentException(CoreResources.ClientContextExtensions_Clone_Url_of_the_site_is_required_, nameof(siteUrl)); | |
} | |
ClientContext clonedClientContext = targetContext; | |
#if !NETSTANDARD2_0 | |
clonedClientContext.AuthenticationMode = clientContext.AuthenticationMode; | |
#endif | |
clonedClientContext.ClientTag = clientContext.ClientTag; | |
#if !SP2013 | |
clonedClientContext.DisableReturnValueCache = clientContext.DisableReturnValueCache; | |
#endif | |
// In case of using networkcredentials in on premises or SharePointOnlineCredentials in Office 365 | |
if (clientContext.Credentials != null) | |
{ | |
clonedClientContext.Credentials = clientContext.Credentials; | |
// In case of existing Event Handlers | |
clonedClientContext.ExecutingWebRequest += (sender, webRequestEventArgs) => | |
{ | |
// Call the ExecutingWebRequest delegate method from the original ClientContext object, but pass along the webRequestEventArgs of | |
// the new delegate method | |
MethodInfo methodInfo = clientContext.GetType().GetMethod("OnExecutingWebRequest", BindingFlags.Instance | BindingFlags.NonPublic); | |
object[] parametersArray = new object[] { webRequestEventArgs }; | |
methodInfo.Invoke(clientContext, parametersArray); | |
}; | |
} | |
else | |
{ | |
// Check if we do have context settings | |
var contextSettings = clientContext.GetContextSettings(); | |
if (contextSettings != null) // We do have more information about this client context, so let's use it to do a more intelligent clone | |
{ | |
string newSiteUrl = siteUrl.ToString(); | |
// A diffent host = different audience ==> new access token is needed | |
if (contextSettings.UsesDifferentAudience(newSiteUrl)) | |
{ | |
// We need to create a new context using a new authentication manager as the token expiration is handled in there | |
OfficeDevPnP.Core.AuthenticationManager authManager = new OfficeDevPnP.Core.AuthenticationManager(); | |
ClientContext newClientContext = null; | |
if (contextSettings.Type == ClientContextType.SharePointACSAppOnly) | |
{ | |
newClientContext = authManager.GetAppOnlyAuthenticatedContext(newSiteUrl, TokenHelper.GetRealmFromTargetUrl(new Uri(newSiteUrl)), contextSettings.ClientId, contextSettings.ClientSecret, contextSettings.AcsHostUrl, contextSettings.GlobalEndPointPrefix); | |
} | |
#if !ONPREMISES | |
else if (contextSettings.Type == ClientContextType.AzureADCredentials) | |
{ | |
newClientContext = authManager.GetAzureADCredentialsContext(newSiteUrl, contextSettings.UserName, contextSettings.Password); | |
} | |
else if (contextSettings.Type == ClientContextType.AzureADCertificate) | |
{ | |
if (contextSettings.Certificate != null) | |
{ | |
newClientContext = authManager.GetAzureADAppOnlyAuthenticatedContext(newSiteUrl, contextSettings.ClientId, contextSettings.Tenant, contextSettings.Certificate, contextSettings.Environment); | |
} | |
else | |
{ | |
newClientContext = authManager.GetAzureADAppOnlyAuthenticatedContext(newSiteUrl, contextSettings.ClientId, contextSettings.Tenant, contextSettings.ClientAssertionCertificate, contextSettings.Environment); | |
} | |
} | |
else if(contextSettings.Type == ClientContextType.Cookie) | |
{ | |
newClientContext = new ClientContext(newSiteUrl); | |
newClientContext.ExecutingWebRequest += (sender, webRequestEventArgs) => | |
{ | |
// Call the ExecutingWebRequest delegate method from the original ClientContext object, but pass along the webRequestEventArgs of | |
// the new delegate method | |
MethodInfo methodInfo = clientContext.GetType().GetMethod("OnExecutingWebRequest", BindingFlags.Instance | BindingFlags.NonPublic); | |
object[] parametersArray = new object[] { webRequestEventArgs }; | |
methodInfo.Invoke(clientContext, parametersArray); | |
}; | |
ClientContextSettings clientContextSettings = new ClientContextSettings() | |
{ | |
Type = ClientContextType.Cookie, | |
SiteUrl = newSiteUrl, | |
}; | |
newClientContext.AddContextSettings(clientContextSettings); | |
} | |
#endif | |
if (newClientContext != null) | |
{ | |
//Take over the form digest handling setting | |
#if !NETSTANDARD2_0 | |
newClientContext.FormDigestHandlingEnabled = (clientContext as ClientContext).FormDigestHandlingEnabled; | |
#endif | |
newClientContext.ClientTag = clientContext.ClientTag; | |
#if !SP2013 | |
newClientContext.DisableReturnValueCache = clientContext.DisableReturnValueCache; | |
#endif | |
return newClientContext; | |
} | |
else | |
{ | |
throw new Exception($"Cloning for context setting type {contextSettings.Type} was not yet implemented"); | |
} | |
} | |
else | |
{ | |
// Take over the context settings, this is needed if we later on want to clone this context to a different audience | |
contextSettings.SiteUrl = newSiteUrl; | |
clonedClientContext.AddContextSettings(contextSettings); | |
clonedClientContext.ExecutingWebRequest += delegate (object oSender, WebRequestEventArgs webRequestEventArgs) | |
{ | |
// Call the ExecutingWebRequest delegate method from the original ClientContext object, but pass along the webRequestEventArgs of | |
// the new delegate method | |
MethodInfo methodInfo = clientContext.GetType().GetMethod("OnExecutingWebRequest", BindingFlags.Instance | BindingFlags.NonPublic); | |
object[] parametersArray = new object[] { webRequestEventArgs }; | |
methodInfo.Invoke(clientContext, parametersArray); | |
}; | |
} | |
} | |
else // Fallback the default cloning logic if there were not context settings available | |
{ | |
//Take over the form digest handling setting | |
#if !NETSTANDARD2_0 | |
clonedClientContext.FormDigestHandlingEnabled = (clientContext as ClientContext).FormDigestHandlingEnabled; | |
#endif | |
var originalUri = new Uri(clientContext.Url); | |
// If the cloned host is not the same as the original one | |
// and if there is an active PnPProvisioningContext | |
if (originalUri.Host != siteUrl.Host && | |
PnPProvisioningContext.Current != null) | |
{ | |
// Let's apply that specific Access Token | |
clonedClientContext.ExecutingWebRequest += (sender, args) => | |
{ | |
// We get a fresh new Access Token for every request, to avoid using an expired one | |
var accessToken = PnPProvisioningContext.Current.AcquireToken(siteUrl.Authority, null); | |
args.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken; | |
}; | |
} | |
// Else if the cloned host is not the same as the original one | |
// and if there is a custom Access Token for it in the input arguments | |
else if (originalUri.Host != siteUrl.Host && | |
accessTokens != null && accessTokens.Count > 0 && | |
accessTokens.ContainsKey(siteUrl.Authority)) | |
{ | |
// Let's apply that specific Access Token | |
clonedClientContext.ExecutingWebRequest += (sender, args) => | |
{ | |
args.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessTokens[siteUrl.Authority]; | |
}; | |
} | |
// Else if the cloned host is not the same as the original one | |
// and if the client context is a PnPClientContext with custom access tokens in its property bag | |
else if (originalUri.Host != siteUrl.Host && | |
accessTokens == null && clientContext is PnPClientContext && | |
((PnPClientContext)clientContext).PropertyBag.ContainsKey("AccessTokens") && | |
((Dictionary<string, string>)((PnPClientContext)clientContext).PropertyBag["AccessTokens"]).ContainsKey(siteUrl.Authority)) | |
{ | |
// Let's apply that specific Access Token | |
clonedClientContext.ExecutingWebRequest += (sender, args) => | |
{ | |
args.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + ((Dictionary<string, string>)((PnPClientContext)clientContext).PropertyBag["AccessTokens"])[siteUrl.Authority]; | |
}; | |
} | |
else | |
{ | |
// In case of app only or SAML | |
clonedClientContext.ExecutingWebRequest += (sender, webRequestEventArgs) => | |
{ | |
// Call the ExecutingWebRequest delegate method from the original ClientContext object, but pass along the webRequestEventArgs of | |
// the new delegate method | |
MethodInfo methodInfo = clientContext.GetType().GetMethod("OnExecutingWebRequest", BindingFlags.Instance | BindingFlags.NonPublic); | |
object[] parametersArray = new object[] { webRequestEventArgs }; | |
methodInfo.Invoke(clientContext, parametersArray); | |
}; | |
} | |
} | |
} | |
return clonedClientContext; | |
} | |
/// <summary> | |
/// Returns the number of pending requests | |
/// </summary> | |
/// <param name="clientContext">Client context to check the pending requests for</param> | |
/// <returns>The number of pending requests</returns> | |
public static int PendingRequestCount(this ClientRuntimeContext clientContext) | |
{ | |
int count = 0; | |
if (clientContext.HasPendingRequest) | |
{ | |
var result = clientContext.PendingRequest.GetType().GetProperty("Actions", BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic); | |
if (result != null) | |
{ | |
var propValue = result.GetValue(clientContext.PendingRequest); | |
if (propValue != null) | |
{ | |
count = (propValue as System.Collections.Generic.List<ClientAction>).Count; | |
} | |
} | |
} | |
return count; | |
} | |
/// <summary> | |
/// Gets a site collection context for the passed web. This site collection client context uses the same credentials | |
/// as the passed client context | |
/// </summary> | |
/// <param name="clientContext">Client context to take the credentials from</param> | |
/// <returns>A site collection client context object for the site collection</returns> | |
public static ClientContext GetSiteCollectionContext(this ClientRuntimeContext clientContext) | |
{ | |
Site site = (clientContext as ClientContext).Site; | |
if (!site.IsObjectPropertyInstantiated("Url")) | |
{ | |
clientContext.Load(site); | |
clientContext.ExecuteQueryRetry(); | |
} | |
return clientContext.Clone(site.Url); | |
} | |
/// <summary> | |
/// Checks if the used ClientContext is app-only | |
/// </summary> | |
/// <param name="clientContext">The ClientContext to inspect</param> | |
/// <returns>True if app-only, false otherwise</returns> | |
public static bool IsAppOnly(this ClientRuntimeContext clientContext) | |
{ | |
// Set initial result to false | |
var result = false; | |
// Try to get an access token from the current context | |
var accessToken = clientContext.GetAccessToken(); | |
// If any | |
if (!String.IsNullOrEmpty(accessToken)) | |
{ | |
// Try to decode the access token | |
var token = new JwtSecurityToken(accessToken); | |
// Search for the UPN claim, to see if we have user's delegation | |
var upn = token.Claims.FirstOrDefault(claim => claim.Type == "upn")?.Value; | |
if (String.IsNullOrEmpty(upn)) | |
{ | |
result = true; | |
} | |
} | |
else if (clientContext.Credentials == null) | |
{ | |
result = true; | |
} | |
// As a final check, do we have the auth cookies? | |
else if (clientContext.HasAuthCookies()) | |
{ | |
result = false; | |
} | |
return (result); | |
} | |
#if ONPREMISES | |
/// <summary> | |
/// Checks if the used ClientContext is app-only | |
/// </summary> | |
/// <param name="clientContext">The ClientContext to inspect</param> | |
/// <returns>True if app-only, false otherwise</returns> | |
public static bool IsAppOnlyWithDelegation(this ClientRuntimeContext clientContext) | |
{ | |
// Set initial result to false | |
var result = false; | |
// Try to get an access token from the current context | |
var accessToken = clientContext.GetAccessToken(); | |
// If any | |
if (!String.IsNullOrEmpty(accessToken)) | |
{ | |
// Try to decode the access token | |
try | |
{ | |
var token = new JwtSecurityToken(accessToken); | |
if (token.Audiences.Any(x => x.StartsWith(TokenHelper.SharePointPrincipal))) | |
{ | |
} | |
// Search for the UPN claim, to see if we have user's delegation | |
var upn = token.Claims.FirstOrDefault(claim => claim.Type == "upn")?.Value; | |
if (!String.IsNullOrEmpty(upn)) | |
{ | |
result = true; | |
} | |
} | |
catch (Exception ex) | |
{ | |
throw new Exception("Maybe Newtonsoft.Json assembly is not loaded?", ex); | |
} | |
} | |
else if (clientContext.Credentials == null) | |
{ | |
result = false; | |
} | |
// As a final check, do we have the auth cookies? | |
if (clientContext.HasAuthCookies()) | |
{ | |
result = false; | |
} | |
return (result); | |
} | |
#endif | |
/// <summary> | |
/// Gets an access token from a <see cref="ClientContext"/> instance. Only works when using an add-in or app-only authentication flow. | |
/// </summary> | |
/// <param name="clientContext"><see cref="ClientContext"/> instance to obtain an access token for</param> | |
/// <returns>Access token for the given <see cref="ClientContext"/> instance</returns> | |
public static string GetAccessToken(this ClientRuntimeContext clientContext) | |
{ | |
string accessToken = null; | |
if (PnPProvisioningContext.Current != null) | |
{ | |
accessToken = PnPProvisioningContext.Current.AcquireToken(new Uri(clientContext.Url).Authority, null); | |
} | |
else | |
{ | |
EventHandler<WebRequestEventArgs> handler = (s, e) => | |
{ | |
string authorization = e.WebRequestExecutor.RequestHeaders["Authorization"]; | |
if (!string.IsNullOrEmpty(authorization)) | |
{ | |
accessToken = authorization.Replace("Bearer ", string.Empty); | |
} | |
}; | |
// Issue a dummy request to get it from the Authorization header | |
clientContext.ExecutingWebRequest += handler; | |
clientContext.ExecuteQueryRetry(); | |
clientContext.ExecutingWebRequest -= handler; | |
} | |
return accessToken; | |
} | |
/// <summary> | |
/// Gets a boolean if the current request contains the FedAuth and rtFa cookies. | |
/// </summary> | |
/// <param name="clientContext"></param> | |
/// <returns></returns> | |
private static bool HasAuthCookies(this ClientRuntimeContext clientContext) | |
{ | |
clientContext.ExecutingWebRequest += ClientContext_ExecutingWebRequestCookieCounter; | |
clientContext.ExecuteQueryRetry(); | |
clientContext.ExecutingWebRequest -= ClientContext_ExecutingWebRequestCookieCounter; | |
return hasAuthCookies; | |
} | |
private static void ClientContext_ExecutingWebRequestCookieCounter(object sender, WebRequestEventArgs e) | |
{ | |
var fedAuth = false; | |
var rtFa = false; | |
if (e.WebRequestExecutor != null && e.WebRequestExecutor.WebRequest != null && e.WebRequestExecutor.WebRequest.CookieContainer != null) | |
{ | |
var cookies = e.WebRequestExecutor.WebRequest.CookieContainer.GetCookies(e.WebRequestExecutor.WebRequest.RequestUri); | |
if (cookies.Count > 0) | |
{ | |
for (var q = 0; q < cookies.Count; q++) | |
{ | |
if (cookies[q].Name == "FedAuth") | |
{ | |
fedAuth = true; | |
} | |
if (cookies[q].Name == "rtFa") | |
{ | |
rtFa = true; | |
} | |
} | |
} | |
} | |
hasAuthCookies = fedAuth && rtFa; | |
} | |
/// <summary> | |
/// Gets the CookieCollection by cookie name = FedAuth or rtFa | |
/// </summary> | |
/// <param name="clientContext"></param> | |
/// <returns></returns> | |
internal static CookieCollection GetCookieCollection(this ClientRuntimeContext clientContext) | |
{ | |
return GetCookieCollection(clientContext, new List<string> | |
{ | |
"FedAuth", "rtFa" | |
}); | |
} | |
/// <summary> | |
/// Gets the CookieCollection by the cookie name. If no cookieNames are passed in it returns all cookies | |
/// </summary> | |
/// <param name="clientContext"></param> | |
/// <param name="cookieNames"></param> | |
/// <returns></returns> | |
internal static CookieCollection GetCookieCollection(this ClientRuntimeContext clientContext, IReadOnlyCollection<string> cookieNames) | |
{ | |
CookieCollection cookieCollection = null; | |
void Handler(object sender, WebRequestEventArgs e) | |
=> cookieCollection = HandleWebRequest(e, cookieNames); | |
clientContext.ExecutingWebRequest += Handler; | |
clientContext.ExecuteQuery(); | |
clientContext.ExecutingWebRequest -= Handler; | |
return cookieCollection; | |
} | |
private static CookieCollection HandleWebRequest(WebRequestEventArgs e, IReadOnlyCollection<string> cookieNames = null) | |
{ | |
var cookieCollection = new CookieCollection(); | |
if (e.WebRequestExecutor?.WebRequest?.CookieContainer == null) | |
{ | |
return null; | |
} | |
var cookies = e.WebRequestExecutor.WebRequest.CookieContainer | |
.GetCookies(e.WebRequestExecutor.WebRequest.RequestUri); | |
if (cookies.Count <= 0) | |
{ | |
return null; | |
} | |
foreach (Cookie cookie in cookies) | |
{ | |
if (cookie == null) | |
{ | |
continue; | |
} | |
if (cookieNames == null || !cookieNames.Any()) | |
{ | |
cookieCollection.Add(cookie); | |
} | |
else if (cookieNames.Any(r => r.Equals(cookie.Name))) | |
{ | |
cookieCollection.Add(cookie); | |
} | |
} | |
return cookieCollection; | |
} | |
/// <summary> | |
/// Defines a Maximum Retry Attemped Exception | |
/// </summary> | |
[Serializable] | |
public class MaximumRetryAttemptedException : Exception | |
{ | |
/// <summary> | |
/// Constructor | |
/// </summary> | |
/// <param name="message"></param> | |
public MaximumRetryAttemptedException(string message) | |
: base(message) | |
{ | |
} | |
} | |
/// <summary> | |
/// Checks the server library version of the context for a minimally required version | |
/// </summary> | |
/// <param name="clientContext">clientContext to operate on</param> | |
/// <param name="minimallyRequiredVersion">provide version to validate</param> | |
/// <returns>True if it has minimal required version, false otherwise</returns> | |
public static bool HasMinimalServerLibraryVersion(this ClientRuntimeContext clientContext, string minimallyRequiredVersion) | |
{ | |
return HasMinimalServerLibraryVersion(clientContext, new Version(minimallyRequiredVersion)); | |
} | |
/// <summary> | |
/// Checks the server library version of the context for a minimally required version | |
/// </summary> | |
/// <param name="clientContext">clientContext to operate on</param> | |
/// <param name="minimallyRequiredVersion">provide version to validate</param> | |
/// <returns>True if it has minimal required version, false otherwise</returns> | |
public static bool HasMinimalServerLibraryVersion(this ClientRuntimeContext clientContext, Version minimallyRequiredVersion) | |
{ | |
bool hasMinimalVersion = false; | |
#if !ONPREMISES | |
try | |
{ | |
clientContext.ExecuteQueryRetry(); | |
hasMinimalVersion = clientContext.ServerLibraryVersion.CompareTo(minimallyRequiredVersion) >= 0; | |
} | |
catch (PropertyOrFieldNotInitializedException) | |
{ | |
// swallow the exception. | |
} | |
#else | |
try | |
{ | |
Uri urlUri = new Uri(clientContext.Url); | |
HttpWebRequest request = (HttpWebRequest)WebRequest.Create($"{urlUri.Scheme}://{urlUri.DnsSafeHost}:{urlUri.Port}/_vti_pvt/service.cnf"); | |
request.UseDefaultCredentials = true; | |
var response = request.GetResponse(); | |
using (var dataStream = response.GetResponseStream()) | |
{ | |
// Open the stream using a StreamReader for easy access. | |
using (System.IO.StreamReader reader = new System.IO.StreamReader(dataStream)) | |
{ | |
// Read the content.Will be in this format | |
// vti_encoding:SR|utf8-nl | |
// vti_extenderversion: SR | 15.0.0.4505 | |
string version = reader.ReadToEnd().Split('|')[2].Trim(); | |
// Only compare the first three digits | |
var compareToVersion = new Version(minimallyRequiredVersion.Major, minimallyRequiredVersion.Minor, minimallyRequiredVersion.Build, 0); | |
hasMinimalVersion = new Version(version.Split('.')[0].ToInt32(), 0, version.Split('.')[3].ToInt32(), 0).CompareTo(compareToVersion) >= 0; | |
} | |
} | |
} | |
catch (WebException ex) | |
{ | |
Log.Warning(Constants.LOGGING_SOURCE, CoreResources.ClientContextExtensions_HasMinimalServerLibraryVersion_Error, ex.ToDetailedString(clientContext)); | |
} | |
#endif | |
return hasMinimalVersion; | |
} | |
/// <summary> | |
/// Returns the name of the method calling ExecuteQueryRetry and ExecuteQueryRetryAsync | |
/// </summary> | |
/// <returns>A string with the method name</returns> | |
private static string GetCallingPnPMethod() | |
{ | |
StackTrace t = new StackTrace(); | |
string pnpMethod = ""; | |
try | |
{ | |
for (int i = 0; i < t.FrameCount; i++) | |
{ | |
var frame = t.GetFrame(i); | |
var frameName = frame.GetMethod().Name; | |
if (frameName.Equals("ExecuteQueryRetry") || frameName.Equals("ExecuteQueryRetryAsync")) | |
{ | |
var method = t.GetFrame(i + 1).GetMethod(); | |
// Only return the calling method in case ExecuteQueryRetry was called from inside the PnP core library | |
if (method.Module.Name.Equals("OfficeDevPnP.Core.dll", StringComparison.InvariantCultureIgnoreCase)) | |
{ | |
pnpMethod = method.Name; | |
} | |
break; | |
} | |
} | |
} | |
catch | |
{ | |
// ignored | |
} | |
return pnpMethod; | |
} | |
/// <summary> | |
/// Returns the request digest from the current session/site | |
/// </summary> | |
/// <param name="context"></param> | |
/// <returns></returns> | |
public static async Task<string> GetRequestDigestAsync(this ClientContext context) | |
{ | |
await new SynchronizationContextRemover(); | |
//InitializeSecurity(context); | |
using (var handler = new HttpClientHandler()) | |
{ | |
string responseString = string.Empty; | |
var accessToken = context.GetAccessToken(); | |
context.Web.EnsureProperty(w => w.Url); | |
if (String.IsNullOrEmpty(accessToken)) | |
{ | |
handler.SetAuthenticationCookies(context); | |
} | |
using (var httpClient = new PnPHttpProvider(handler)) | |
{ | |
string requestUrl = String.Format("{0}/_api/contextinfo", context.Web.Url); | |
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUrl); | |
request.Headers.Add("accept", "application/json;odata=verbose"); | |
if (!string.IsNullOrEmpty(accessToken)) | |
{ | |
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); | |
} | |
else | |
{ | |
if (context.Credentials is NetworkCredential networkCredential) | |
{ | |
handler.Credentials = networkCredential; | |
} | |
} | |
HttpResponseMessage response = await httpClient.SendAsync(request); | |
if (response.IsSuccessStatusCode) | |
{ | |
responseString = await response.Content.ReadAsStringAsync(); | |
} | |
else | |
{ | |
var errorSb = new System.Text.StringBuilder(); | |
errorSb.AppendLine(await response.Content.ReadAsStringAsync()); | |
if (response.Headers.Contains("SPRequestGuid")) | |
{ | |
var values = response.Headers.GetValues("SPRequestGuid"); | |
if (values != null) | |
{ | |
var spRequestGuid = values.FirstOrDefault(); | |
errorSb.AppendLine($"ServerErrorTraceCorrelationId: {spRequestGuid}"); | |
} | |
} | |
throw new Exception(errorSb.ToString()); | |
} | |
} | |
var contextInformation = JsonConvert.DeserializeObject<dynamic>(responseString); | |
string formDigestValue = contextInformation.d.GetContextWebInformation.FormDigestValue; | |
return await Task.Run(() => formDigestValue).ConfigureAwait(false); | |
} | |
} | |
private static void Context_ExecutingWebRequest(object sender, WebRequestEventArgs e) | |
{ | |
if (!String.IsNullOrEmpty(e.WebRequestExecutor.RequestHeaders.Get("Authorization"))) | |
{ | |
accessToken = e.WebRequestExecutor.RequestHeaders.Get("Authorization").Replace("Bearer ", ""); | |
} | |
} | |
#if !SP2013 && !SP2016 | |
/// <summary> | |
/// BETA: Creates a Communication Site Collection | |
/// </summary> | |
/// <param name="clientContext"></param> | |
/// <param name="siteCollectionCreationInformation"></param> | |
/// <returns></returns> | |
public static async Task<ClientContext> CreateSiteAsync(this ClientContext clientContext, CommunicationSiteCollectionCreationInformation siteCollectionCreationInformation) | |
{ | |
await new SynchronizationContextRemover(); | |
return await SiteCollection.CreateAsync(clientContext, siteCollectionCreationInformation); | |
} | |
/// <summary> | |
/// BETA: Creates a Team Site Collection with no group | |
/// </summary> | |
/// <param name="clientContext"></param> | |
/// <param name="siteCollectionCreationInformation"></param> | |
/// <returns></returns> | |
public static async Task<ClientContext> CreateSiteAsync(this ClientContext clientContext, TeamNoGroupSiteCollectionCreationInformation siteCollectionCreationInformation) | |
{ | |
await new SynchronizationContextRemover(); | |
return await SiteCollection.CreateAsync(clientContext, siteCollectionCreationInformation); | |
} | |
#endif | |
#if !ONPREMISES | |
/// <summary> | |
/// BETA: Creates a Team Site Collection | |
/// </summary> | |
/// <param name="clientContext"></param> | |
/// <param name="siteCollectionCreationInformation"></param> | |
/// <returns></returns> | |
public static async Task<ClientContext> CreateSiteAsync(this ClientContext clientContext, TeamSiteCollectionCreationInformation siteCollectionCreationInformation) | |
{ | |
await new SynchronizationContextRemover(); | |
return await SiteCollection.CreateAsync(clientContext, siteCollectionCreationInformation); | |
} | |
/// <summary> | |
/// BETA: Groupifies a classic Team Site Collection | |
/// </summary> | |
/// <param name="clientContext">ClientContext instance of the site to be groupified</param> | |
/// <param name="siteCollectionGroupifyInformation">Information needed to groupify this site</param> | |
/// <returns>The clientcontext of the groupified site</returns> | |
public static async Task<ClientContext> GroupifySiteAsync(this ClientContext clientContext, TeamSiteCollectionGroupifyInformation siteCollectionGroupifyInformation) | |
{ | |
await new SynchronizationContextRemover(); | |
return await SiteCollection.GroupifyAsync(clientContext, siteCollectionGroupifyInformation); | |
} | |
/// <summary> | |
/// Checks if an alias is already used for an office 365 group or not | |
/// </summary> | |
/// <param name="clientContext">ClientContext of the site to operate against</param> | |
/// <param name="alias">Alias to verify</param> | |
/// <returns>True if in use, false otherwise</returns> | |
public static async Task<bool> AliasExistsAsync(this ClientContext clientContext, string alias) | |
{ | |
await new SynchronizationContextRemover(); | |
return await SiteCollection.AliasExistsAsync(clientContext, alias); | |
} | |
/// <summary> | |
/// Enable MS Teams team on a group connected team site | |
/// </summary> | |
/// <param name="clientContext"></param> | |
/// <returns></returns> | |
public static async Task<string> TeamifyAsync(this ClientContext clientContext) | |
{ | |
await new SynchronizationContextRemover(); | |
return await SiteCollection.TeamifySiteAsync(clientContext); | |
} | |
/// <summary> | |
/// Checks whether the teamify prompt is hidden in O365 Group connected sites | |
/// </summary> | |
/// <param name="clientContext">ClientContext of the site to operate against</param> | |
/// <returns></returns> | |
public static async Task<bool> IsTeamifyPromptHiddenAsync(this ClientContext clientContext) | |
{ | |
await new SynchronizationContextRemover(); | |
return await SiteCollection.IsTeamifyPromptHiddenAsync(clientContext); | |
} | |
[Obsolete("Use IsTeamifyPromptHiddenAsync")] | |
public static async Task<bool> IsTeamifyPromptHidden(this ClientContext clientContext) | |
{ | |
return await IsTeamifyPromptHiddenAsync(clientContext); | |
} | |
/// <summary> | |
/// Hide the teamify prompt displayed in O365 group connected sites | |
/// </summary> | |
/// <param name="clientContext">ClientContext of the site to operate against</param> | |
/// <returns></returns> | |
public static async Task<bool> HideTeamifyPromptAsync(this ClientContext clientContext) | |
{ | |
await new SynchronizationContextRemover(); | |
return await SiteCollection.HideTeamifyPromptAsync(clientContext); | |
} | |
/// <summary> | |
/// Deletes a Communication site or a group-less Modern team site | |
/// </summary> | |
/// <param name="clientContext"></param> | |
/// <returns></returns> | |
public static async Task<bool> DeleteSiteAsync(this ClientContext clientContext) | |
{ | |
await new SynchronizationContextRemover(); | |
return await SiteCollection.DeleteSiteAsync(clientContext); | |
} | |
#endif | |
} | |
} |