2018-02-13 09:23:53 -08:00
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
2016-07-29 13:02:49 -07:00
2018-11-29 23:13:19 +05:00
using System.Collections.Generic ;
2016-03-30 14:20:52 -07:00
using System.Collections.ObjectModel ;
using System.ComponentModel ;
2018-11-29 23:13:19 +05:00
using System.Diagnostics ;
using System.Globalization ;
using System.IO ;
using System.IO.Compression ;
2017-12-21 14:27:49 -08:00
using System.Management.Automation.Configuration ;
2016-03-30 14:20:52 -07:00
using System.Management.Automation.Internal ;
2018-11-29 23:13:19 +05:00
using System.Net ;
using System.Net.Http ;
2016-03-30 14:20:52 -07:00
using System.Runtime.Serialization ;
2018-11-29 23:13:19 +05:00
using System.Security ;
2016-03-30 14:20:52 -07:00
using System.Text ;
2018-11-29 23:13:19 +05:00
using System.Threading ;
using System.Threading.Tasks ;
using System.Xml ;
using System.Xml.Schema ;
2016-03-30 14:20:52 -07:00
using Microsoft.PowerShell.Commands ;
using Microsoft.Win32 ;
namespace System.Management.Automation.Help
{
/// <summary>
2019-01-06 04:40:24 +05:00
/// Updatable help system exception.
2016-03-30 14:20:52 -07:00
/// </summary>
[Serializable]
internal class UpdatableHelpSystemException : Exception
{
/// <summary>
2019-01-06 04:40:24 +05:00
/// Class constructor.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-28 16:39:06 +05:00
/// <param name="errorId">FullyQualifiedErrorId.</param>
2018-12-29 08:25:49 +05:00
/// <param name="message">Exception message.</param>
/// <param name="cat">Category.</param>
/// <param name="targetObject">Target object.</param>
/// <param name="innerException">Inner exception.</param>
2016-03-30 14:20:52 -07:00
internal UpdatableHelpSystemException ( string errorId , string message , ErrorCategory cat , object targetObject , Exception innerException )
: base ( message , innerException )
{
2016-08-02 13:29:09 -07:00
FullyQualifiedErrorId = errorId ;
ErrorCategory = cat ;
TargetObject = targetObject ;
2016-03-30 14:20:52 -07:00
}
#if ! CORECLR
/// <summary>
2019-01-06 04:40:24 +05:00
/// Class constructor.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="serializationInfo">Serialization info.</param>
/// <param name="streamingContext">Streaming context.</param>
2016-03-30 14:20:52 -07:00
protected UpdatableHelpSystemException ( SerializationInfo serializationInfo , StreamingContext streamingContext )
: base ( serializationInfo , streamingContext )
{
}
#endif
/// <summary>
2019-01-06 04:40:24 +05:00
/// Fully qualified error id.
2016-03-30 14:20:52 -07:00
/// </summary>
2016-08-02 13:29:09 -07:00
internal string FullyQualifiedErrorId { get ; }
2016-03-30 14:20:52 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// Error category.
2016-03-30 14:20:52 -07:00
/// </summary>
2016-08-02 13:29:09 -07:00
internal ErrorCategory ErrorCategory { get ; }
2016-03-30 14:20:52 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// Target object.
2016-03-30 14:20:52 -07:00
/// </summary>
2016-08-02 13:29:09 -07:00
internal object TargetObject { get ; }
2016-03-30 14:20:52 -07:00
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Exception context.
2016-03-30 14:20:52 -07:00
/// </summary>
internal class UpdatableHelpExceptionContext
{
/// <summary>
2019-01-06 04:40:24 +05:00
/// Class constructor.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="exception">Exception to wrap.</param>
2016-03-30 14:20:52 -07:00
internal UpdatableHelpExceptionContext ( UpdatableHelpSystemException exception )
{
2016-08-02 13:29:09 -07:00
Exception = exception ;
Modules = new HashSet < string >( StringComparer . OrdinalIgnoreCase );
Cultures = new HashSet < string >( StringComparer . OrdinalIgnoreCase );
2016-03-30 14:20:52 -07:00
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// A list of modules.
2016-03-30 14:20:52 -07:00
/// </summary>
2016-08-02 13:29:09 -07:00
internal HashSet < string > Modules { get ; set ; }
2016-03-30 14:20:52 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// A list of UI cultures.
2016-03-30 14:20:52 -07:00
/// </summary>
2016-08-02 13:29:09 -07:00
internal HashSet < string > Cultures { get ; set ; }
2016-03-30 14:20:52 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// Gets the help system exception.
2016-03-30 14:20:52 -07:00
/// </summary>
2016-08-02 13:29:09 -07:00
internal UpdatableHelpSystemException Exception { get ; }
2016-03-30 14:20:52 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// Creates an error record from this context.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="commandType">Command type.</param>
2018-12-25 21:43:03 +05:00
/// <returns>Error record.</returns>
2016-03-30 14:20:52 -07:00
internal ErrorRecord CreateErrorRecord ( UpdatableHelpCommandType commandType )
{
2016-08-02 13:29:09 -07:00
Debug . Assert ( Modules . Count != 0 );
2016-03-30 14:20:52 -07:00
2016-08-02 13:29:09 -07:00
return new ErrorRecord ( new Exception ( GetExceptionMessage ( commandType )), Exception . FullyQualifiedErrorId , Exception . ErrorCategory ,
Exception . TargetObject );
2016-03-30 14:20:52 -07:00
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Gets the exception message.
2016-03-30 14:20:52 -07:00
/// </summary>
/// <param name="commandType"></param>
/// <returns></returns>
internal string GetExceptionMessage ( UpdatableHelpCommandType commandType )
{
2018-06-13 00:30:10 +05:00
string message = string . Empty ;
2016-08-02 13:29:09 -07:00
SortedSet < string > sortedModules = new SortedSet < string >( Modules , StringComparer . CurrentCultureIgnoreCase );
SortedSet < string > sortedCultures = new SortedSet < string >( Cultures , StringComparer . CurrentCultureIgnoreCase );
2018-12-31 20:44:16 +05:00
string modules = string . Join ( ", " , sortedModules );
string cultures = string . Join ( ", " , sortedCultures );
2016-03-30 14:20:52 -07:00
if ( commandType == UpdatableHelpCommandType . UpdateHelpCommand )
{
2016-08-02 13:29:09 -07:00
if ( Cultures . Count == 0 )
2016-03-30 14:20:52 -07:00
{
2016-08-02 13:29:09 -07:00
message = StringUtil . Format ( HelpDisplayStrings . FailedToUpdateHelpForModule , modules , Exception . Message );
2016-03-30 14:20:52 -07:00
}
else
{
2016-08-02 13:29:09 -07:00
message = StringUtil . Format ( HelpDisplayStrings . FailedToUpdateHelpForModuleWithCulture , modules , cultures , Exception . Message );
2016-03-30 14:20:52 -07:00
}
}
else
{
2016-08-02 13:29:09 -07:00
if ( Cultures . Count == 0 )
2016-03-30 14:20:52 -07:00
{
2016-08-02 13:29:09 -07:00
message = StringUtil . Format ( HelpDisplayStrings . FailedToSaveHelpForModule , modules , Exception . Message );
2016-03-30 14:20:52 -07:00
}
else
{
2016-08-02 13:29:09 -07:00
message = StringUtil . Format ( HelpDisplayStrings . FailedToSaveHelpForModuleWithCulture , modules , cultures , Exception . Message );
2016-03-30 14:20:52 -07:00
}
}
return message ;
}
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Enumeration showing Update or Save help.
2016-03-30 14:20:52 -07:00
/// </summary>
internal enum UpdatableHelpCommandType
{
UnknownCommand = 0 ,
UpdateHelpCommand = 1 ,
SaveHelpCommand = 2
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Progress event arguments.
2016-03-30 14:20:52 -07:00
/// </summary>
internal class UpdatableHelpProgressEventArgs : EventArgs
{
/// <summary>
2019-01-06 04:40:24 +05:00
/// Class constructor.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="moduleName">Module name.</param>
/// <param name="status">Progress status.</param>
/// <param name="percent">Progress percentage.</param>
2016-03-30 14:20:52 -07:00
internal UpdatableHelpProgressEventArgs ( string moduleName , string status , int percent )
{
2018-12-31 15:10:15 +05:00
Debug . Assert (! string . IsNullOrEmpty ( status ));
2016-03-30 14:20:52 -07:00
2016-08-02 13:29:09 -07:00
CommandType = UpdatableHelpCommandType . UnknownCommand ;
ProgressStatus = status ;
ProgressPercent = percent ;
ModuleName = moduleName ;
2016-03-30 14:20:52 -07:00
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Class constructor.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="moduleName">Module name.</param>
/// <param name="type">Command type.</param>
/// <param name="status">Progress status.</param>
/// <param name="percent">Progress percentage.</param>
2016-03-30 14:20:52 -07:00
internal UpdatableHelpProgressEventArgs ( string moduleName , UpdatableHelpCommandType type , string status , int percent )
{
2018-12-31 15:10:15 +05:00
Debug . Assert (! string . IsNullOrEmpty ( status ));
2016-03-30 14:20:52 -07:00
2016-08-02 13:29:09 -07:00
CommandType = type ;
ProgressStatus = status ;
ProgressPercent = percent ;
ModuleName = moduleName ;
2016-03-30 14:20:52 -07:00
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Progress status.
2016-03-30 14:20:52 -07:00
/// </summary>
2016-08-02 13:29:09 -07:00
internal string ProgressStatus { get ; }
2016-03-30 14:20:52 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// Progress percentage.
2016-03-30 14:20:52 -07:00
/// </summary>
2016-08-02 13:29:09 -07:00
internal int ProgressPercent { get ; }
2016-03-30 14:20:52 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// Module name.
2016-03-30 14:20:52 -07:00
/// </summary>
2016-08-02 13:29:09 -07:00
internal string ModuleName { get ; }
2016-03-30 14:20:52 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// Command type.
2016-03-30 14:20:52 -07:00
/// </summary>
2016-08-02 13:29:09 -07:00
internal UpdatableHelpCommandType CommandType { get ; set ; }
2016-03-30 14:20:52 -07:00
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// This class implements the Updatable Help System common operations.
2016-03-30 14:20:52 -07:00
/// </summary>
internal class UpdatableHelpSystem : IDisposable
{
private TimeSpan _defaultTimeout ;
private Collection < UpdatableHelpProgressEventArgs > _progressEvents ;
private bool _stopping ;
private object _syncObject ;
private UpdatableHelpCommandBase _cmdlet ;
private CancellationTokenSource _cancelTokenSource ;
2016-08-02 13:29:09 -07:00
internal WebClient WebClient { get ; }
2016-03-30 14:20:52 -07:00
2016-08-02 13:29:09 -07:00
internal string CurrentModule { get ; set ; }
2016-03-30 14:20:52 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// Class constructor.
2016-03-30 14:20:52 -07:00
/// </summary>
internal UpdatableHelpSystem ( UpdatableHelpCommandBase cmdlet , bool useDefaultCredentials )
{
2016-08-02 13:29:09 -07:00
WebClient = new WebClient ();
2016-03-30 14:20:52 -07:00
_defaultTimeout = new TimeSpan ( 0 , 0 , 30 );
_progressEvents = new Collection < UpdatableHelpProgressEventArgs >();
2016-08-02 13:29:09 -07:00
Errors = new Collection < Exception >();
2016-03-30 14:20:52 -07:00
_stopping = false ;
_syncObject = new object ();
_cmdlet = cmdlet ;
_cancelTokenSource = new CancellationTokenSource ();
2016-08-02 13:29:09 -07:00
WebClient . UseDefaultCredentials = useDefaultCredentials ;
2016-03-30 14:20:52 -07:00
#if ! CORECLR
2016-08-02 13:29:09 -07:00
WebClient . DownloadProgressChanged += new DownloadProgressChangedEventHandler ( HandleDownloadProgressChanged );
WebClient . DownloadFileCompleted += new AsyncCompletedEventHandler ( HandleDownloadFileCompleted );
2016-03-30 14:20:52 -07:00
#endif
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Disposes the help system.
2016-03-30 14:20:52 -07:00
/// </summary>
public void Dispose ()
{
#if ! CORECLR
_completionEvent . Dispose ();
#endif
_cancelTokenSource . Dispose ();
2016-08-02 13:29:09 -07:00
WebClient . Dispose ();
2016-03-30 14:20:52 -07:00
GC . SuppressFinalize ( this );
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Help system errors.
2016-03-30 14:20:52 -07:00
/// </summary>
2016-08-02 13:29:09 -07:00
internal Collection < Exception > Errors { get ; }
2016-03-30 14:20:52 -07:00
/// <summary>
/// Gets the current UIculture (includes the fallback chain)
/// </summary>
2018-12-25 21:43:03 +05:00
/// <returns>A list of cultures.</returns>
2016-03-30 14:20:52 -07:00
internal IEnumerable < string > GetCurrentUICulture ()
{
CultureInfo culture = CultureInfo . CurrentUICulture ;
while ( culture != null )
{
if ( string . IsNullOrEmpty ( culture . Name ))
{
yield break ;
}
yield return culture . Name ;
culture = culture . Parent ;
}
yield break ;
}
#region Help Metadata Retrieval
/// <summary>
2019-01-06 04:40:24 +05:00
/// Gets an internal help URI.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="module">Internal module information.</param>
/// <param name="culture">Help content culture.</param>
2018-12-25 21:43:03 +05:00
/// <returns>Internal help uri representation.</returns>
2016-03-30 14:20:52 -07:00
internal UpdatableHelpUri GetHelpInfoUri ( UpdatableHelpModuleInfo module , CultureInfo culture )
{
return new UpdatableHelpUri ( module . ModuleName , module . ModuleGuid , culture , ResolveUri ( module . HelpInfoUri , false ));
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Gets the HelpInfo xml from the given URI.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="commandType">Command type.</param>
2018-12-28 16:39:06 +05:00
/// <param name="uri">HelpInfo URI.</param>
2018-12-29 08:25:49 +05:00
/// <param name="moduleName">Module name.</param>
/// <param name="moduleGuid">Module GUID.</param>
/// <param name="culture">Current UI culture.</param>
2018-12-25 21:43:03 +05:00
/// <returns>HelpInfo object.</returns>
2016-03-30 14:20:52 -07:00
internal UpdatableHelpInfo GetHelpInfo ( UpdatableHelpCommandType commandType , string uri , string moduleName , Guid moduleGuid , string culture )
{
try
{
2016-08-02 13:29:09 -07:00
OnProgressChanged ( this , new UpdatableHelpProgressEventArgs ( CurrentModule , commandType , StringUtil . Format (
2016-03-30 14:20:52 -07:00
HelpDisplayStrings . UpdateProgressLocating ), 0 ));
string xml ;
using ( HttpClientHandler handler = new HttpClientHandler ())
{
2016-08-02 13:29:09 -07:00
handler . UseDefaultCredentials = WebClient . UseDefaultCredentials ;
2016-03-30 14:20:52 -07:00
using ( HttpClient client = new HttpClient ( handler ))
{
client . Timeout = _defaultTimeout ;
Task < string > responseBody = client . GetStringAsync ( uri );
2016-07-29 13:02:49 -07:00
xml = responseBody . Result ;
2016-03-30 14:20:52 -07:00
if ( responseBody . Exception != null )
{
return null ;
}
}
}
2018-11-29 23:13:19 +05:00
2016-07-29 13:02:49 -07:00
UpdatableHelpInfo helpInfo = CreateHelpInfo ( xml , moduleName , moduleGuid ,
currentCulture : culture , pathOverride : null , verbose : true ,
shouldResolveUri : true , ignoreValidationException : false );
2016-03-30 14:20:52 -07:00
return helpInfo ;
}
#if ! CORECLR
catch ( WebException )
{
return null ;
}
#endif
finally
{
2016-08-02 13:29:09 -07:00
OnProgressChanged ( this , new UpdatableHelpProgressEventArgs ( CurrentModule , commandType , StringUtil . Format (
2016-03-30 14:20:52 -07:00
HelpDisplayStrings . UpdateProgressLocating ), 100 ));
}
}
/// <summary>
/// Sends a standard HTTP request to get the resolved URI (potential FwLinks)
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="baseUri">Base URI.</param>
2016-03-30 14:20:52 -07:00
/// <param name="verbose"></param>
2018-12-25 21:43:03 +05:00
/// <returns>Resolved URI.</returns>
2016-03-30 14:20:52 -07:00
private string ResolveUri ( string baseUri , bool verbose )
{
2018-12-31 15:10:15 +05:00
Debug . Assert (! string . IsNullOrEmpty ( baseUri ));
2016-03-30 14:20:52 -07:00
// Directory.Exists checks if baseUri is a network drive or
// a local directory. If baseUri is local, we don't need to resolve it.
2017-01-16 13:31:14 -08:00
//
2016-03-30 14:20:52 -07:00
// The / check works because all of our fwlinks must resolve
// to a remote virtual directory. I think HTTP always appends /
// in reference to a directory.
// Like if you send a request to www.technet.com/powershell you will get
// a 301/203 response with the response URI set to www.technet.com/powershell/
//
if ( Directory . Exists ( baseUri ) || baseUri . EndsWith ( "/" , StringComparison . OrdinalIgnoreCase ))
{
if ( verbose )
{
_cmdlet . WriteVerbose ( StringUtil . Format ( RemotingErrorIdStrings . URIRedirectWarningToHost , baseUri ));
}
return baseUri ;
}
if ( verbose )
{
_cmdlet . WriteVerbose ( StringUtil . Format ( HelpDisplayStrings . UpdateHelpResolveUriVerbose , baseUri ));
}
string uri = baseUri ;
try
{
// We only allow 10 redirections
for ( int i = 0 ; i < 10 ; i ++)
{
if ( _stopping )
{
return uri ;
}
using ( HttpClientHandler handler = new HttpClientHandler ())
{
handler . AllowAutoRedirect = false ;
2016-08-02 13:29:09 -07:00
handler . UseDefaultCredentials = WebClient . UseDefaultCredentials ;
2016-03-30 14:20:52 -07:00
using ( HttpClient client = new HttpClient ( handler ))
{
client . Timeout = new TimeSpan ( 0 , 0 , 30 ); // Set 30 second timeout
Task < HttpResponseMessage > responseMessage = client . GetAsync ( uri );
using ( HttpResponseMessage response = responseMessage . Result )
{
if ( response . StatusCode == HttpStatusCode . Found ||
response . StatusCode == HttpStatusCode . Redirect ||
response . StatusCode == HttpStatusCode . Moved ||
response . StatusCode == HttpStatusCode . MovedPermanently )
{
Uri responseUri = response . Headers . Location ;
if ( responseUri . IsAbsoluteUri )
{
uri = responseUri . ToString ();
}
else
{
Uri originalAbs = new Uri ( uri );
uri = uri . Replace ( originalAbs . AbsolutePath , responseUri . ToString ());
}
uri = uri . Trim ();
if ( verbose )
{
_cmdlet . WriteVerbose ( StringUtil . Format ( RemotingErrorIdStrings . URIRedirectWarningToHost , uri ));
}
if ( uri . EndsWith ( "/" , StringComparison . OrdinalIgnoreCase ))
{
return uri ;
}
}
else if ( response . StatusCode == HttpStatusCode . OK )
{
if ( uri . EndsWith ( "/" , StringComparison . OrdinalIgnoreCase ))
{
return uri ;
}
else
{
throw new UpdatableHelpSystemException ( "InvalidHelpInfoUri" , StringUtil . Format ( HelpDisplayStrings . InvalidHelpInfoUri , uri ),
ErrorCategory . InvalidOperation , null , null );
}
}
}
}
}
}
}
catch ( UriFormatException e )
{
throw new UpdatableHelpSystemException ( "InvalidUriFormat" , e . Message , ErrorCategory . InvalidData , null , e );
}
throw new UpdatableHelpSystemException ( "TooManyRedirections" , StringUtil . Format ( HelpDisplayStrings . TooManyRedirections ),
ErrorCategory . InvalidOperation , null , null );
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// HelpInfo.xml schema.
2016-03-30 14:20:52 -07:00
/// </summary>
private const string HelpInfoXmlSchema = @"<?xml version=""1.0"" encoding=""utf-8""?>
<xs:schema attributeFormDefault=""unqualified"" elementFormDefault=""qualified""
targetNamespace=""http://schemas.microsoft.com/powershell/help/2010/05"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
<xs:element name=""HelpInfo"">
<xs:complexType>
<xs:sequence>
<xs:element name=""HelpContentURI"" type=""xs:anyURI"" minOccurs=""1"" maxOccurs=""1"" />
<xs:element name=""SupportedUICultures"" minOccurs=""1"" maxOccurs=""1"">
<xs:complexType>
<xs:sequence>
<xs:element name=""UICulture"" minOccurs=""1"" maxOccurs=""unbounded"">
<xs:complexType>
<xs:sequence>
<xs:element name=""UICultureName"" type=""xs:language"" minOccurs=""1"" maxOccurs=""1"" />
<xs:element name=""UICultureVersion"" type=""xs:string"" minOccurs=""1"" maxOccurs=""1"" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>" ;
private const string HelpInfoXmlNamespace = "http://schemas.microsoft.com/powershell/help/2010/05" ;
private const string HelpInfoXmlValidationFailure = "HelpInfoXmlValidationFailure" ;
/// <summary>
2019-01-06 04:40:24 +05:00
/// Creates a HelpInfo object.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-28 16:39:06 +05:00
/// <param name="xml">XML text.</param>
2018-12-29 08:25:49 +05:00
/// <param name="moduleName">Module name.</param>
/// <param name="moduleGuid">Module GUID.</param>
/// <param name="currentCulture">Current UI cultures.</param>
/// <param name="pathOverride">Overrides the path contained within HelpInfo.xml.</param>
2016-03-30 14:20:52 -07:00
/// <param name="verbose"></param>
/// <param name="shouldResolveUri">
/// Resolve the uri retrieved from the <paramref name="xml"/> content. The uri is resolved
/// to handle redirections if any.
/// </param>
2018-12-29 08:25:49 +05:00
/// <param name="ignoreValidationException">Ignore the xsd validation exception and return null in such case.</param>
2018-12-25 21:43:03 +05:00
/// <returns>HelpInfo object.</returns>
2016-03-30 14:20:52 -07:00
internal UpdatableHelpInfo CreateHelpInfo ( string xml , string moduleName , Guid moduleGuid ,
string currentCulture , string pathOverride , bool verbose , bool shouldResolveUri , bool ignoreValidationException )
{
XmlDocument document = null ;
try
{
2016-07-29 13:02:49 -07:00
document = CreateValidXmlDocument ( xml , HelpInfoXmlNamespace , HelpInfoXmlSchema ,
new ValidationEventHandler ( HelpInfoValidationHandler ),
2016-03-30 14:20:52 -07:00
true );
}
catch ( UpdatableHelpSystemException e )
{
if ( ignoreValidationException && HelpInfoXmlValidationFailure . Equals ( e . FullyQualifiedErrorId , StringComparison . Ordinal ))
{
return null ;
}
throw ;
}
catch ( XmlException e )
{
if ( ignoreValidationException ) { return null ; }
throw new UpdatableHelpSystemException ( HelpInfoXmlValidationFailure ,
e . Message , ErrorCategory . InvalidData , null , e );
}
string uri = pathOverride ;
string unresolvedUri = document [ "HelpInfo" ][ "HelpContentURI" ]. InnerText ;
2018-12-31 15:10:15 +05:00
if ( string . IsNullOrEmpty ( pathOverride ))
2016-03-30 14:20:52 -07:00
{
if ( shouldResolveUri )
{
uri = ResolveUri ( unresolvedUri , verbose );
}
else
{
2016-07-29 13:02:49 -07:00
uri = unresolvedUri ;
2016-03-30 14:20:52 -07:00
}
}
XmlNodeList cultures = document [ "HelpInfo" ][ "SupportedUICultures" ]. ChildNodes ;
CultureSpecificUpdatableHelp [] updatableHelpItem = new CultureSpecificUpdatableHelp [ cultures . Count ];
for ( int i = 0 ; i < cultures . Count ; i ++)
{
updatableHelpItem [ i ] = new CultureSpecificUpdatableHelp (
new CultureInfo ( cultures [ i ][ "UICultureName" ]. InnerText ),
new Version ( cultures [ i ][ "UICultureVersion" ]. InnerText ));
}
UpdatableHelpInfo helpInfo = new UpdatableHelpInfo ( unresolvedUri , updatableHelpItem );
2018-12-31 15:10:15 +05:00
if (! string . IsNullOrEmpty ( currentCulture ))
2016-03-30 14:20:52 -07:00
{
WildcardOptions wildcardOptions = WildcardOptions . IgnoreCase | WildcardOptions . CultureInvariant ;
IEnumerable < WildcardPattern > patternList = SessionStateUtilities . CreateWildcardsFromStrings ( new string [ 1 ] { currentCulture }, wildcardOptions );
for ( int i = 0 ; i < updatableHelpItem . Length ; i ++)
{
if ( SessionStateUtilities . MatchesAnyWildcardPattern ( updatableHelpItem [ i ]. Culture . Name , patternList , true ))
{
helpInfo . HelpContentUriCollection . Add ( new UpdatableHelpUri ( moduleName , moduleGuid , updatableHelpItem [ i ]. Culture , uri ));
}
}
}
2018-12-31 15:10:15 +05:00
if (! string . IsNullOrEmpty ( currentCulture ) && helpInfo . HelpContentUriCollection . Count == 0 )
2016-03-30 14:20:52 -07:00
{
// throw exception
throw new UpdatableHelpSystemException ( "HelpCultureNotSupported" ,
StringUtil . Format ( HelpDisplayStrings . HelpCultureNotSupported ,
currentCulture , helpInfo . GetSupportedCultures ()), ErrorCategory . InvalidOperation , null , null );
}
return helpInfo ;
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Creates a valid xml document.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="xml">Input xml.</param>
/// <param name="ns">Schema namespace.</param>
/// <param name="schema">Xml schema.</param>
/// <param name="handler">Validation event handler.</param>
2016-03-30 14:20:52 -07:00
/// <param name="helpInfo">HelpInfo or HelpContent?</param>
private XmlDocument CreateValidXmlDocument ( string xml , string ns , string schema , ValidationEventHandler handler ,
bool helpInfo )
{
XmlReaderSettings settings = new XmlReaderSettings ();
settings . Schemas . Add ( ns , new XmlTextReader ( new StringReader ( schema )));
settings . ValidationType = ValidationType . Schema ;
XmlReader reader = XmlReader . Create ( new StringReader ( xml ), settings );
XmlDocument document = new XmlDocument ();
try
{
document . Load ( reader );
document . Validate ( handler );
}
catch ( XmlSchemaValidationException e )
{
if ( helpInfo )
{
throw new UpdatableHelpSystemException ( HelpInfoXmlValidationFailure ,
StringUtil . Format ( HelpDisplayStrings . HelpInfoXmlValidationFailure , e . Message ),
ErrorCategory . InvalidData , null , e );
}
else
{
throw new UpdatableHelpSystemException ( "HelpContentXmlValidationFailure" ,
StringUtil . Format ( HelpDisplayStrings . HelpContentXmlValidationFailure , e . Message ),
ErrorCategory . InvalidData , null , e );
}
}
2018-12-23 22:20:06 -08:00
2016-03-30 14:20:52 -07:00
return document ;
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Handles HelpInfo XML validation events.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="sender">Event sender.</param>
/// <param name="arg">Event arguments.</param>
2016-03-30 14:20:52 -07:00
private void HelpInfoValidationHandler ( object sender , ValidationEventArgs arg )
{
switch ( arg . Severity )
{
case XmlSeverityType . Error :
{
throw new UpdatableHelpSystemException ( HelpInfoXmlValidationFailure ,
StringUtil . Format ( HelpDisplayStrings . HelpInfoXmlValidationFailure ),
ErrorCategory . InvalidData , null , arg . Exception );
}
case XmlSeverityType . Warning :
break ;
}
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Handles Help content MAML validation events.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="sender">Event sender.</param>
/// <param name="arg">Event arguments.</param>
2016-03-30 14:20:52 -07:00
private void HelpContentValidationHandler ( object sender , ValidationEventArgs arg )
{
switch ( arg . Severity )
{
case XmlSeverityType . Error :
{
throw new UpdatableHelpSystemException ( "HelpContentXmlValidationFailure" ,
StringUtil . Format ( HelpDisplayStrings . HelpContentXmlValidationFailure ),
ErrorCategory . InvalidData , null , arg . Exception );
}
case XmlSeverityType . Warning :
break ;
}
}
#endregion
#region Help Content Retrieval
/// <summary>
2019-01-06 04:40:24 +05:00
/// Cancels all asynchronous download operations.
2016-03-30 14:20:52 -07:00
/// </summary>
internal void CancelDownload ()
{
_cancelTokenSource . Cancel ();
_stopping = true ;
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Downloads and installs help content.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="commandType">Command type.</param>
/// <param name="context">Execution context.</param>
/// <param name="destPaths">Destination paths.</param>
/// <param name="fileName">File names.</param>
/// <param name="culture">Culture to update.</param>
/// <param name="helpContentUri">Help content uri.</param>
/// <param name="xsdPath">Path of the maml XSDs.</param>
/// <param name="installed">Files installed.</param>
2018-12-25 21:43:03 +05:00
/// <returns>True if the operation succeeded, false if not.</returns>
2016-03-30 14:20:52 -07:00
internal bool DownloadAndInstallHelpContent ( UpdatableHelpCommandType commandType , ExecutionContext context , Collection < string > destPaths ,
string fileName , CultureInfo culture , string helpContentUri , string xsdPath , out Collection < string > installed )
{
if ( _stopping )
{
installed = new Collection < string >();
return false ;
}
string cache = Path . Combine ( Path . GetTempPath (), Path . GetFileNameWithoutExtension ( Path . GetRandomFileName ()));
if (! DownloadHelpContent ( commandType , cache , helpContentUri , fileName , culture . Name ))
{
installed = new Collection < string >();
return false ;
}
InstallHelpContent ( commandType , context , cache , destPaths , fileName , cache , culture , xsdPath , out installed );
return true ;
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Downloads the help content.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="commandType">Command type.</param>
/// <param name="path">Destination path.</param>
/// <param name="helpContentUri">Help content uri.</param>
/// <param name="fileName">Combined file name.</param>
/// <param name="culture">Culture name.</param>
2018-12-25 21:43:03 +05:00
/// <returns>True if the operation succeeded, false if not.</returns>
2016-03-30 14:20:52 -07:00
internal bool DownloadHelpContent ( UpdatableHelpCommandType commandType , string path , string helpContentUri , string fileName , string culture )
{
if ( _stopping )
{
return false ;
}
if (! Directory . Exists ( path ))
{
Directory . CreateDirectory ( path );
}
2016-08-02 13:29:09 -07:00
OnProgressChanged ( this , new UpdatableHelpProgressEventArgs ( CurrentModule , commandType , StringUtil . Format (
2016-03-30 14:20:52 -07:00
HelpDisplayStrings . UpdateProgressConnecting ), 0 ));
string uri = helpContentUri + fileName ;
return DownloadHelpContentHttpClient ( uri , Path . Combine ( path , fileName ), commandType );
}
/// <summary>
2018-11-29 23:13:19 +05:00
/// Downloads the help content and saves it to a directory.
2016-03-30 14:20:52 -07:00
/// </summary>
/// <param name="uri"></param>
/// <param name="fileName"></param>
/// <param name="commandType"></param>
/// <returns></returns>
private bool DownloadHelpContentHttpClient ( string uri , string fileName , UpdatableHelpCommandType commandType )
{
// TODO: Was it intentional for them to remove IDisposable from Task?
using ( HttpClientHandler handler = new HttpClientHandler ())
{
handler . AllowAutoRedirect = false ;
2016-08-02 13:29:09 -07:00
handler . UseDefaultCredentials = WebClient . UseDefaultCredentials ;
2016-03-30 14:20:52 -07:00
using ( HttpClient client = new HttpClient ( handler ))
{
client . Timeout = _defaultTimeout ;
Task < HttpResponseMessage > responseMsg = client . GetAsync ( new Uri ( uri ), _cancelTokenSource . Token );
// TODO: Should I use a continuation to write the stream to a file?
responseMsg . Wait ();
if ( _stopping )
{
return true ;
}
if (! responseMsg . IsCanceled )
{
if ( responseMsg . Exception != null )
{
2016-08-02 13:29:09 -07:00
Errors . Add ( new UpdatableHelpSystemException ( "HelpContentNotFound" ,
2016-03-30 14:20:52 -07:00
StringUtil . Format ( HelpDisplayStrings . HelpContentNotFound ),
ErrorCategory . ResourceUnavailable , null , responseMsg . Exception ));
}
else
{
lock ( _syncObject )
{
2016-08-02 13:29:09 -07:00
_progressEvents . Add ( new UpdatableHelpProgressEventArgs ( CurrentModule , StringUtil . Format (
2016-03-30 14:20:52 -07:00
HelpDisplayStrings . UpdateProgressDownloading ), 100 ));
}
// Write the stream to the specified file to achieve functional parity with WebClient.DownloadFileAsync().
HttpResponseMessage response = responseMsg . Result ;
if ( response . IsSuccessStatusCode )
{
WriteResponseToFile ( response , fileName );
}
else
{
2016-08-02 13:29:09 -07:00
Errors . Add ( new UpdatableHelpSystemException ( "HelpContentNotFound" ,
2016-03-30 14:20:52 -07:00
StringUtil . Format ( HelpDisplayStrings . HelpContentNotFound ),
ErrorCategory . ResourceUnavailable , null , responseMsg . Exception ));
}
}
}
2018-12-23 22:20:06 -08:00
2016-03-30 14:20:52 -07:00
SendProgressEvents ( commandType );
}
}
2018-12-23 22:20:06 -08:00
2016-08-02 13:29:09 -07:00
return ( Errors . Count == 0 );
2016-03-30 14:20:52 -07:00
}
/// <summary>
/// Writes the content of an HTTP response to the specified file.
/// </summary>
/// <param name="response"></param>
/// <param name="fileName"></param>
private void WriteResponseToFile ( HttpResponseMessage response , string fileName )
{
// TODO: Settings to use? FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite
using ( FileStream downloadedFileStream = new FileStream ( fileName , FileMode . Create , FileAccess . Write ))
{
Task copyStreamOp = response . Content . CopyToAsync ( downloadedFileStream );
copyStreamOp . Wait ();
if ( copyStreamOp . Exception != null )
{
2016-08-02 13:29:09 -07:00
Errors . Add ( copyStreamOp . Exception );
2016-03-30 14:20:52 -07:00
}
}
}
private void SendProgressEvents ( UpdatableHelpCommandType commandType )
{
// Send progress events
lock ( _syncObject )
{
if ( _progressEvents . Count > 0 )
{
2016-09-01 14:03:25 +00:00
foreach ( UpdatableHelpProgressEventArgs evt in _progressEvents )
2016-03-30 14:20:52 -07:00
{
2016-09-01 14:03:25 +00:00
evt . CommandType = commandType ;
2016-03-30 14:20:52 -07:00
2016-09-01 14:03:25 +00:00
OnProgressChanged ( this , evt );
2016-03-30 14:20:52 -07:00
}
_progressEvents . Clear ();
}
}
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Installs HelpInfo.xml.
2016-03-30 14:20:52 -07:00
/// </summary>
/// <param name="moduleName"></param>
/// <param name="moduleGuid"></param>
2018-12-29 08:25:49 +05:00
/// <param name="culture">Culture updated.</param>
/// <param name="version">Version updated.</param>
/// <param name="contentUri">Help content uri.</param>
/// <param name="destPath">Destination name.</param>
/// <param name="fileName">Combined file name.</param>
/// <param name="force">Forces the file to copy.</param>
2016-03-30 14:20:52 -07:00
internal void GenerateHelpInfo ( string moduleName , Guid moduleGuid , string contentUri , string culture , Version version , string destPath , string fileName , bool force )
{
Debug . Assert ( Directory . Exists ( destPath ));
if ( _stopping )
{
return ;
}
string destHelpInfo = Path . Combine ( destPath , fileName );
if ( force )
{
RemoveReadOnly ( destHelpInfo );
}
UpdatableHelpInfo oldHelpInfo = null ;
string xml = UpdatableHelpSystem . LoadStringFromPath ( _cmdlet , destHelpInfo , null );
if ( xml != null )
{
// constructing the helpinfo object from previous update help log xml..
// no need to resolve the uri's in this case.
2016-07-29 13:02:49 -07:00
oldHelpInfo = CreateHelpInfo ( xml , moduleName , moduleGuid , currentCulture : null , pathOverride : null ,
verbose : false , shouldResolveUri : false , ignoreValidationException : force );
2016-03-30 14:20:52 -07:00
}
using ( FileStream file = new FileStream ( destHelpInfo , FileMode . Create , FileAccess . Write ))
{
XmlWriterSettings settings = new XmlWriterSettings ();
settings . Encoding = Encoding . UTF8 ;
settings . Indent = true ; // Default indentation is two spaces
using ( XmlWriter writer = XmlWriter . Create ( file , settings ))
{
writer . WriteStartDocument ();
writer . WriteStartElement ( "HelpInfo" , "http://schemas.microsoft.com/powershell/help/2010/05" );
writer . WriteStartElement ( "HelpContentURI" );
writer . WriteValue ( contentUri );
writer . WriteEndElement ();
writer . WriteStartElement ( "SupportedUICultures" );
bool found = false ;
if ( oldHelpInfo != null )
{
foreach ( CultureSpecificUpdatableHelp oldInfo in oldHelpInfo . UpdatableHelpItems )
{
if ( oldInfo . Culture . Name . Equals ( culture , StringComparison . OrdinalIgnoreCase ))
{
if ( oldInfo . Version . Equals ( version ))
{
writer . WriteStartElement ( "UICulture" );
writer . WriteStartElement ( "UICultureName" );
writer . WriteValue ( oldInfo . Culture . Name );
writer . WriteEndElement ();
writer . WriteStartElement ( "UICultureVersion" );
writer . WriteValue ( oldInfo . Version . ToString ());
writer . WriteEndElement ();
writer . WriteEndElement ();
}
else
{
writer . WriteStartElement ( "UICulture" );
writer . WriteStartElement ( "UICultureName" );
writer . WriteValue ( culture );
writer . WriteEndElement ();
writer . WriteStartElement ( "UICultureVersion" );
writer . WriteValue ( version . ToString ());
writer . WriteEndElement ();
writer . WriteEndElement ();
}
found = true ;
}
else
{
writer . WriteStartElement ( "UICulture" );
writer . WriteStartElement ( "UICultureName" );
writer . WriteValue ( oldInfo . Culture . Name );
writer . WriteEndElement ();
writer . WriteStartElement ( "UICultureVersion" );
writer . WriteValue ( oldInfo . Version . ToString ());
writer . WriteEndElement ();
writer . WriteEndElement ();
}
}
}
if (! found )
{
writer . WriteStartElement ( "UICulture" );
writer . WriteStartElement ( "UICultureName" );
writer . WriteValue ( culture );
writer . WriteEndElement ();
writer . WriteStartElement ( "UICultureVersion" );
writer . WriteValue ( version . ToString ());
writer . WriteEndElement ();
writer . WriteEndElement ();
}
writer . WriteEndElement ();
writer . WriteEndDocument ();
}
}
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Removes the read only attribute.
2016-03-30 14:20:52 -07:00
/// </summary>
/// <param name="path"></param>
private void RemoveReadOnly ( string path )
{
if ( File . Exists ( path ))
{
FileAttributes attributes = File . GetAttributes ( path );
2016-07-29 13:02:49 -07:00
if (( attributes & FileAttributes . ReadOnly ) == FileAttributes . ReadOnly )
2016-03-30 14:20:52 -07:00
{
attributes = ( attributes & ~ FileAttributes . ReadOnly );
File . SetAttributes ( path , attributes );
}
}
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Installs (unzips) the help content.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="commandType">Command type.</param>
/// <param name="context">Execution context.</param>
/// <param name="sourcePath">Source directory.</param>
/// <param name="destPaths">Destination paths.</param>
/// <param name="fileName">Help content file name.</param>
/// <param name="tempPath">Temporary path.</param>
/// <param name="culture">Current culture.</param>
/// <param name="xsdPath">Path of the maml XSDs.</param>
/// <param name="installed">Files installed.</param>
2016-03-30 14:20:52 -07:00
/// <remarks>
/// Directory pointed by <paramref name="tempPath"/> (if any) will be deleted.
/// </remarks>
internal void InstallHelpContent ( UpdatableHelpCommandType commandType , ExecutionContext context , string sourcePath ,
Collection < string > destPaths , string fileName , string tempPath , CultureInfo culture , string xsdPath ,
out Collection < string > installed )
{
installed = new Collection < string >();
if ( _stopping )
{
installed = new Collection < string >();
return ;
}
// These paths must exist
if (! Directory . Exists ( tempPath ))
{
Directory . CreateDirectory ( tempPath );
}
Debug . Assert ( destPaths . Count > 0 );
try
{
2016-08-02 13:29:09 -07:00
OnProgressChanged ( this , new UpdatableHelpProgressEventArgs ( CurrentModule , commandType , StringUtil . Format (
2016-03-30 14:20:52 -07:00
HelpDisplayStrings . UpdateProgressInstalling ), 0 ));
2016-10-12 13:06:10 -07:00
string combinedSourcePath = GetFilePath ( Path . Combine ( sourcePath , fileName ));
2016-03-30 14:20:52 -07:00
2016-10-12 13:06:10 -07:00
if ( string . IsNullOrEmpty ( combinedSourcePath ))
2016-03-30 14:20:52 -07:00
{
throw new UpdatableHelpSystemException ( "HelpContentNotFound" , StringUtil . Format ( HelpDisplayStrings . HelpContentNotFound ),
ErrorCategory . ResourceUnavailable , null , null );
}
string combinedTempPath = Path . Combine ( tempPath , Path . GetFileNameWithoutExtension ( fileName ));
if ( Directory . Exists ( combinedTempPath ))
{
Directory . Delete ( combinedTempPath , true );
}
bool needToCopy = true ;
UnzipHelpContent ( context , combinedSourcePath , combinedTempPath , out needToCopy );
if ( needToCopy )
{
ValidateAndCopyHelpContent ( combinedTempPath , destPaths , culture . Name , xsdPath , out installed );
}
}
finally
{
2016-08-02 13:29:09 -07:00
OnProgressChanged ( this , new UpdatableHelpProgressEventArgs ( CurrentModule , commandType , StringUtil . Format (
2016-03-30 14:20:52 -07:00
HelpDisplayStrings . UpdateProgressInstalling ), 100 ));
try
{
if ( Directory . Exists ( tempPath ))
{
Directory . Delete ( tempPath );
}
}
2016-07-29 13:02:49 -07:00
catch ( IOException ) { }
2016-03-30 14:20:52 -07:00
catch ( UnauthorizedAccessException ) { }
2016-07-29 13:02:49 -07:00
catch ( ArgumentException ) { }
2016-03-30 14:20:52 -07:00
}
}
2016-10-12 13:06:10 -07:00
#if UNIX
private bool ExpandArchive ( string source , string destination )
{
bool sucessfulDecompression = false ;
try
{
2018-11-13 11:30:03 -08:00
using ( ZipArchive zipArchive = ZipFile . Open ( source , ZipArchiveMode . Read ))
2016-10-12 13:06:10 -07:00
{
2018-11-13 11:30:03 -08:00
zipArchive . ExtractToDirectory ( destination );
2016-10-12 13:06:10 -07:00
sucessfulDecompression = true ;
}
}
catch ( ArgumentException ) { }
catch ( InvalidDataException ) { }
catch ( NotSupportedException ) { }
catch ( FileNotFoundException ) { }
catch ( IOException ) { }
catch ( SecurityException ) { }
catch ( UnauthorizedAccessException ) { }
catch ( ObjectDisposedException ) { }
return sucessfulDecompression ;
}
#endif
2016-03-30 14:20:52 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// Unzips to help content to a given location.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="context">Execution context.</param>
/// <param name="srcPath">Source path.</param>
/// <param name="destPath">Destination path.</param>
2018-12-28 16:39:06 +05:00
/// <param name="needToCopy">Is set to false if we find a single file placeholder.txt in cab. This means we no longer need to install help files.</param>
2016-03-30 14:20:52 -07:00
private void UnzipHelpContent ( ExecutionContext context , string srcPath , string destPath , out bool needToCopy )
{
needToCopy = true ;
if (! Directory . Exists ( destPath ))
{
Directory . CreateDirectory ( destPath );
}
2016-10-12 13:06:10 -07:00
string sourceDirectory = Path . GetDirectoryName ( srcPath );
bool sucessfulDecompression = false ;
#if UNIX
sucessfulDecompression = ExpandArchive ( Path . Combine ( sourceDirectory , Path . GetFileName ( srcPath )), destPath );
#else
2016-08-28 05:48:06 +00:00
// Cabinet API doesn't handle the trailing back slash
2016-10-12 13:06:10 -07:00
if (! sourceDirectory . EndsWith ( "\\" , StringComparison . Ordinal ))
2016-03-30 14:20:52 -07:00
{
2016-10-12 13:06:10 -07:00
sourceDirectory += "\\" ;
2016-03-30 14:20:52 -07:00
}
if (! destPath . EndsWith ( "\\" , StringComparison . Ordinal ))
{
destPath += "\\" ;
}
2017-01-16 13:31:14 -08:00
2016-10-12 13:06:10 -07:00
sucessfulDecompression = CabinetExtractorFactory . GetCabinetExtractor (). Extract ( Path . GetFileName ( srcPath ), sourceDirectory , destPath );
#endif
if (! sucessfulDecompression )
2016-03-30 14:20:52 -07:00
{
throw new UpdatableHelpSystemException ( "UnableToExtract" , StringUtil . Format ( HelpDisplayStrings . UnzipFailure ),
ErrorCategory . InvalidOperation , null , null );
}
string [] files = Directory . GetFiles ( destPath );
if ( files . Length == 1 )
{
// If there is a single file
string file = Path . GetFileName ( files [ 0 ]);
if (! string . IsNullOrEmpty ( file ) && file . Equals ( "placeholder.txt" , StringComparison . OrdinalIgnoreCase ))
{
// And that single file is named "placeholder.txt"
var fileInfo = new FileInfo ( files [ 0 ]);
if ( fileInfo . Length == 0 )
{
// And if that single file has length 0, then we delete that file and no longer install help
needToCopy = false ;
try
{
File . Delete ( files [ 0 ]);
string directory = Path . GetDirectoryName ( files [ 0 ]);
if (! string . IsNullOrEmpty ( directory ))
{
Directory . Delete ( directory );
}
}
catch ( FileNotFoundException )
2016-07-29 13:02:49 -07:00
{ }
2016-03-30 14:20:52 -07:00
catch ( DirectoryNotFoundException )
2016-07-29 13:02:49 -07:00
{ }
2016-03-30 14:20:52 -07:00
catch ( UnauthorizedAccessException )
2016-07-29 13:02:49 -07:00
{ }
2016-03-30 14:20:52 -07:00
catch ( System . Security . SecurityException )
2016-07-29 13:02:49 -07:00
{ }
2016-03-30 14:20:52 -07:00
catch ( ArgumentNullException )
2016-07-29 13:02:49 -07:00
{ }
2016-03-30 14:20:52 -07:00
catch ( ArgumentException )
2016-07-29 13:02:49 -07:00
{ }
2016-03-30 14:20:52 -07:00
catch ( PathTooLongException )
2016-07-29 13:02:49 -07:00
{ }
2016-03-30 14:20:52 -07:00
catch ( NotSupportedException )
2016-07-29 13:02:49 -07:00
{ }
2016-03-30 14:20:52 -07:00
catch ( IOException )
2016-07-29 13:02:49 -07:00
{ }
2016-03-30 14:20:52 -07:00
}
}
}
else
{
foreach ( string file in files )
{
if ( File . Exists ( file ))
{
FileInfo fInfo = new FileInfo ( file );
if (( fInfo . Attributes & FileAttributes . ReadOnly ) == FileAttributes . ReadOnly )
{
// Clear the read-only attribute
fInfo . Attributes &= ~( FileAttributes . ReadOnly );
}
}
}
}
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Validates all XML files within a given path.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="sourcePath">Path containing files to validate.</param>
/// <param name="destPaths">Destination paths.</param>
/// <param name="culture">Culture name.</param>
/// <param name="xsdPath">Path of the maml XSDs.</param>
/// <param name="installed">Installed files.</param>
2016-03-30 14:20:52 -07:00
private void ValidateAndCopyHelpContent ( string sourcePath , Collection < string > destPaths , string culture , string xsdPath ,
out Collection < string > installed )
{
installed = new Collection < string >();
string xsd = LoadStringFromPath ( _cmdlet , xsdPath , null );
// We only accept txt files and xml files
foreach ( string file in Directory . GetFiles ( sourcePath ))
{
2018-12-31 20:44:16 +05:00
if (! string . Equals ( Path . GetExtension ( file ), ".xml" , StringComparison . OrdinalIgnoreCase )
&& ! string . Equals ( Path . GetExtension ( file ), ".txt" , StringComparison . OrdinalIgnoreCase ))
2016-03-30 14:20:52 -07:00
{
throw new UpdatableHelpSystemException ( "HelpContentContainsInvalidFiles" ,
StringUtil . Format ( HelpDisplayStrings . HelpContentContainsInvalidFiles ), ErrorCategory . InvalidData ,
null , null );
}
}
// xml validation
foreach ( string file in Directory . GetFiles ( sourcePath ))
{
2018-12-31 20:44:16 +05:00
if ( string . Equals ( Path . GetExtension ( file ), ".xml" , StringComparison . OrdinalIgnoreCase ))
2016-03-30 14:20:52 -07:00
{
if ( xsd == null )
{
2016-07-29 13:02:49 -07:00
throw new ItemNotFoundException ( StringUtil . Format ( HelpDisplayStrings . HelpContentXsdNotFound , xsdPath ));
2016-03-30 14:20:52 -07:00
}
else
{
string xml = LoadStringFromPath ( _cmdlet , file , null );
XmlReader documentReader = XmlReader . Create ( new StringReader ( xml ));
XmlDocument contentDocument = new XmlDocument ();
contentDocument . Load ( documentReader );
if ( contentDocument . ChildNodes . Count != 1 && contentDocument . ChildNodes . Count != 2 )
{
throw new UpdatableHelpSystemException ( "HelpContentXmlValidationFailure" ,
StringUtil . Format ( HelpDisplayStrings . HelpContentXmlValidationFailure , HelpDisplayStrings . RootElementMustBeHelpItems ),
ErrorCategory . InvalidData , null , null );
}
XmlNode helpItemsNode = null ;
2016-07-29 13:02:49 -07:00
if ( contentDocument . DocumentElement != null &&
2016-03-30 14:20:52 -07:00
contentDocument . DocumentElement . LocalName . Equals ( "providerHelp" , StringComparison . OrdinalIgnoreCase ))
{
helpItemsNode = contentDocument ;
}
else
{
if ( contentDocument . ChildNodes . Count == 1 )
{
if (! contentDocument . ChildNodes [ 0 ]. LocalName . Equals ( "helpItems" , StringComparison . OrdinalIgnoreCase ))
{
throw new UpdatableHelpSystemException ( "HelpContentXmlValidationFailure" ,
StringUtil . Format ( HelpDisplayStrings . HelpContentXmlValidationFailure , HelpDisplayStrings . RootElementMustBeHelpItems ),
ErrorCategory . InvalidData , null , null );
}
else
{
helpItemsNode = contentDocument . ChildNodes [ 0 ];
}
}
else if ( contentDocument . ChildNodes . Count == 2 )
{
if (! contentDocument . ChildNodes [ 1 ]. LocalName . Equals ( "helpItems" , StringComparison . OrdinalIgnoreCase ))
{
throw new UpdatableHelpSystemException ( "HelpContentXmlValidationFailure" ,
StringUtil . Format ( HelpDisplayStrings . HelpContentXmlValidationFailure , HelpDisplayStrings . RootElementMustBeHelpItems ),
ErrorCategory . InvalidData , null , null );
}
else
{
helpItemsNode = contentDocument . ChildNodes [ 1 ];
}
}
}
Debug . Assert ( helpItemsNode != null , "helpItemsNode must not be null" );
string targetNamespace = "http://schemas.microsoft.com/maml/2004/10" ;
foreach ( XmlNode node in helpItemsNode . ChildNodes )
{
if ( node . NodeType == XmlNodeType . Element )
2016-07-29 13:02:49 -07:00
{
2016-03-30 14:20:52 -07:00
if (! node . LocalName . Equals ( "providerHelp" , StringComparison . OrdinalIgnoreCase ))
{
if ( node . LocalName . Equals ( "para" , StringComparison . OrdinalIgnoreCase ))
{
if (! node . NamespaceURI . Equals ( "http://schemas.microsoft.com/maml/2004/10" , StringComparison . OrdinalIgnoreCase ))
{
throw new UpdatableHelpSystemException ( "HelpContentXmlValidationFailure" ,
StringUtil . Format ( HelpDisplayStrings . HelpContentXmlValidationFailure ,
StringUtil . Format ( HelpDisplayStrings . HelpContentMustBeInTargetNamespace , targetNamespace )), ErrorCategory . InvalidData , null , null );
}
else
{
continue ;
}
}
2016-07-29 13:02:49 -07:00
2016-03-30 14:20:52 -07:00
if (! node . NamespaceURI . Equals ( "http://schemas.microsoft.com/maml/dev/command/2004/10" , StringComparison . OrdinalIgnoreCase ) &&
2016-07-29 13:02:49 -07:00
! node . NamespaceURI . Equals ( "http://schemas.microsoft.com/maml/dev/dscResource/2004/10" , StringComparison . OrdinalIgnoreCase ))
2016-03-30 14:20:52 -07:00
{
throw new UpdatableHelpSystemException ( "HelpContentXmlValidationFailure" ,
StringUtil . Format ( HelpDisplayStrings . HelpContentXmlValidationFailure ,
StringUtil . Format ( HelpDisplayStrings . HelpContentMustBeInTargetNamespace , targetNamespace )), ErrorCategory . InvalidData , null , null );
}
}
2016-07-29 13:02:49 -07:00
CreateValidXmlDocument ( node . OuterXml , targetNamespace , xsd ,
new ValidationEventHandler ( HelpContentValidationHandler ),
2016-03-30 14:20:52 -07:00
false );
}
}
}
}
2018-12-31 20:44:16 +05:00
else if ( string . Equals ( Path . GetExtension ( file ), ".txt" , StringComparison . OrdinalIgnoreCase ))
2016-03-30 14:20:52 -07:00
{
FileStream fileStream = new FileStream ( file , FileMode . Open , FileAccess . Read );
if ( fileStream . Length > 2 )
{
byte [] firstTwoBytes = new byte [ 2 ];
fileStream . Read ( firstTwoBytes , 0 , 2 );
2016-08-08 22:33:57 -07:00
// Check for Mark Zbikowski's magic initials
2016-03-30 14:20:52 -07:00
if ( firstTwoBytes [ 0 ] == 'M' && firstTwoBytes [ 1 ] == 'Z' )
{
throw new UpdatableHelpSystemException ( "HelpContentContainsInvalidFiles" ,
StringUtil . Format ( HelpDisplayStrings . HelpContentContainsInvalidFiles ), ErrorCategory . InvalidData ,
null , null );
}
}
}
foreach ( string path in destPaths )
{
Debug . Assert ( Directory . Exists ( path ));
string combinedPath = Path . Combine ( path , culture );
if (! Directory . Exists ( combinedPath ))
{
Directory . CreateDirectory ( combinedPath );
}
string destPath = Path . Combine ( combinedPath , Path . GetFileName ( file ));
// Make the destpath writeable if force is used
FileAttributes ? originalFileAttributes = null ;
try
{
if ( File . Exists ( destPath ) && ( _cmdlet . Force ))
{
FileInfo fInfo = new FileInfo ( destPath );
if (( fInfo . Attributes & FileAttributes . ReadOnly ) == FileAttributes . ReadOnly )
{
// remember to reset the read-only attribute later
originalFileAttributes = fInfo . Attributes ;
// Clear the read-only attribute
fInfo . Attributes &= ~( FileAttributes . ReadOnly );
}
}
File . Copy ( file , destPath , true );
}
finally
{
if ( originalFileAttributes . HasValue )
{
File . SetAttributes ( destPath , originalFileAttributes . Value );
}
}
installed . Add ( destPath );
}
}
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Loads string from the given path.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="cmdlet">Cmdlet instance.</param>
/// <param name="path">Path to load.</param>
/// <param name="credential">Credential.</param>
2018-12-25 21:43:03 +05:00
/// <returns>String loaded.</returns>
2016-08-02 19:00:50 -07:00
internal static string LoadStringFromPath ( PSCmdlet cmdlet , string path , PSCredential credential )
2016-03-30 14:20:52 -07:00
{
Debug . Assert ( path != null );
if ( credential != null )
{
// New PSDrive
using ( UpdatableHelpSystemDrive drive = new UpdatableHelpSystemDrive ( cmdlet , Path . GetDirectoryName ( path ), credential ))
{
string tempPath = Path . Combine ( Path . GetTempPath (), Path . GetFileNameWithoutExtension ( Path . GetTempFileName ()));
if (! cmdlet . InvokeProvider . Item . Exists ( Path . Combine ( drive . DriveName , Path . GetFileName ( path ))))
{
return null ;
}
cmdlet . InvokeProvider . Item . Copy ( new string [ 1 ] { Path . Combine ( drive . DriveName , Path . GetFileName ( path )) }, tempPath ,
false , CopyContainers . CopyTargetContainer , true , true );
path = tempPath ;
}
}
2016-10-12 13:06:10 -07:00
string filePath = GetFilePath ( path );
if (! string . IsNullOrEmpty ( filePath ))
2016-03-30 14:20:52 -07:00
{
2016-10-12 13:06:10 -07:00
using ( FileStream currentHelpInfoFile = new FileStream ( filePath , FileMode . Open , FileAccess . Read ))
2016-03-30 14:20:52 -07:00
{
StreamReader reader = new StreamReader ( currentHelpInfoFile );
return reader . ReadToEnd ();
}
}
return null ;
}
2016-10-12 13:06:10 -07:00
/// <summary>
/// Validate the given path. If it exists, return the full path to the file.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
internal static string GetFilePath ( string path )
{
2018-07-25 22:02:09 +05:00
FileInfo item = new FileInfo ( path );
// We use 'FileInfo.Attributes' (not 'FileInfo.Exist')
// because we want to get exceptions
// like UnauthorizedAccessException or IOException.
if (( int ) item . Attributes != - 1 )
{
return path ;
}
2016-10-12 13:06:10 -07:00
#if UNIX
// On Linux, file paths are case sensitive.
// The user does not have control over the files (HelpInfo.xml, .zip, and cab) that are generated by the Publishing team.
// The logic below is to support updating help content via sourcepath parameter for case insensitive files.
2018-07-25 22:02:09 +05:00
var dirInfo = item . Directory ;
2016-10-12 13:06:10 -07:00
string fileName = item . Name ;
// Prerequisite: The directory in the given path must exist and it is case sensitive.
2018-07-25 22:02:09 +05:00
if ( dirInfo . Exists )
2016-10-12 13:06:10 -07:00
{
// Get the list of files in the directory.
2018-07-25 22:02:09 +05:00
FileInfo [] fileList = dirInfo . GetFiles ( searchPattern : fileName , new System . IO . EnumerationOptions { MatchCasing = MatchCasing . CaseInsensitive });
if ( fileList . Length > 0 )
2016-10-12 13:06:10 -07:00
{
2018-07-25 22:02:09 +05:00
return fileList [ 0 ]. FullName ;
2016-10-12 13:06:10 -07:00
}
}
#endif
return null ;
}
2016-03-30 14:20:52 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// Gets the default source path from GP.
2016-03-30 14:20:52 -07:00
/// </summary>
/// <returns></returns>
internal string GetDefaultSourcePath ()
{
2018-01-03 12:38:38 -08:00
var updatableHelpSetting = Utils . GetPolicySetting < UpdatableHelp >( Utils . SystemWideOnlyConfig );
string defaultSourcePath = updatableHelpSetting ?. DefaultSourcePath ;
2018-12-31 15:10:15 +05:00
return string . IsNullOrEmpty ( defaultSourcePath ) ? null : defaultSourcePath ;
2016-03-30 14:20:52 -07:00
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Sets the DisablePromptToUpdatableHelp regkey.
2016-03-30 14:20:52 -07:00
/// </summary>
2016-08-02 19:00:50 -07:00
internal static void SetDisablePromptToUpdateHelp ()
2016-03-30 14:20:52 -07:00
{
2016-08-02 21:36:26 -07:00
try
{
2017-12-21 14:27:49 -08:00
PowerShellConfig . Instance . SetDisablePromptToUpdateHelp ( true );
2016-08-02 21:36:26 -07:00
}
catch ( UnauthorizedAccessException )
{
// Ignore AccessDenied related exceptions
}
catch ( SecurityException )
{
// Ignore AccessDenied related exceptions
}
2016-03-30 14:20:52 -07:00
}
2016-07-29 13:02:49 -07:00
2016-03-30 14:20:52 -07:00
/// <summary>
2019-01-06 04:40:24 +05:00
/// Checks if it is necessary to prompt to update help.
2016-03-30 14:20:52 -07:00
/// </summary>
/// <returns></returns>
2016-08-02 19:00:50 -07:00
internal static bool ShouldPromptToUpdateHelp ()
2016-03-30 14:20:52 -07:00
{
2016-08-08 22:33:57 -07:00
#if UNIX
// TODO: This workaround needs to be removed once updatable help
// works on Linux.
return false ;
#else
2016-03-30 14:20:52 -07:00
try
{
if (! Utils . IsAdministrator ())
{
return false ;
}
2017-12-21 14:27:49 -08:00
return PowerShellConfig . Instance . GetDisablePromptToUpdateHelp ();
2016-03-30 14:20:52 -07:00
}
catch ( SecurityException )
{
return false ;
}
2016-08-08 22:33:57 -07:00
#endif
2016-03-30 14:20:52 -07:00
}
#endregion
#region Events
internal event EventHandler < UpdatableHelpProgressEventArgs > OnProgressChanged ;
#if ! CORECLR
/// <summary>
2019-01-06 04:40:24 +05:00
/// Handles the download completion event.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="sender">Event sender.</param>
/// <param name="e">Event arguments.</param>
2016-03-30 14:20:52 -07:00
private void HandleDownloadFileCompleted ( object sender , AsyncCompletedEventArgs e )
{
if ( _stopping )
{
return ;
}
if (! e . Cancelled )
{
if ( e . Error != null )
{
if ( e . Error is WebException )
{
2016-08-02 13:29:09 -07:00
Errors . Add ( new UpdatableHelpSystemException ( "HelpContentNotFound" , StringUtil . Format ( HelpDisplayStrings . HelpContentNotFound , e . UserState . ToString ()),
2016-03-30 14:20:52 -07:00
ErrorCategory . ResourceUnavailable , null , null ));
}
else
{
2016-08-02 13:29:09 -07:00
Errors . Add ( e . Error );
2016-03-30 14:20:52 -07:00
}
}
else
{
lock ( _syncObject )
{
2016-08-02 13:29:09 -07:00
_progressEvents . Add ( new UpdatableHelpProgressEventArgs ( CurrentModule , StringUtil . Format (
2016-03-30 14:20:52 -07:00
HelpDisplayStrings . UpdateProgressDownloading ), 100 ));
}
}
_completed = true ;
_completionEvent . Set ();
}
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Handles the download progress changed event.
2016-03-30 14:20:52 -07:00
/// </summary>
2018-12-29 08:25:49 +05:00
/// <param name="sender">Event sender.</param>
/// <param name="e">Event arguments.</param>
2016-03-30 14:20:52 -07:00
private void HandleDownloadProgressChanged ( object sender , DownloadProgressChangedEventArgs e )
{
if ( _stopping )
{
return ;
}
lock ( _syncObject )
{
2016-08-02 13:29:09 -07:00
_progressEvents . Add ( new UpdatableHelpProgressEventArgs ( CurrentModule , StringUtil . Format (
2016-03-30 14:20:52 -07:00
HelpDisplayStrings . UpdateProgressDownloading ), e . ProgressPercentage ));
}
_completionEvent . Set ();
}
#endif
#endregion
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Controls the updatable help system drive.
2016-03-30 14:20:52 -07:00
/// </summary>
internal class UpdatableHelpSystemDrive : IDisposable
{
private string _driveName ;
private PSCmdlet _cmdlet ;
/// <summary>
2019-01-06 04:40:24 +05:00
/// Gets the drive name.
2016-03-30 14:20:52 -07:00
/// </summary>
internal string DriveName
{
get
{
return _driveName + ":\\" ;
}
}
/// <summary>
/// </summary>
/// <param name="cmdlet"></param>
/// <param name="path"></param>
/// <param name="credential"></param>
internal UpdatableHelpSystemDrive ( PSCmdlet cmdlet , string path , PSCredential credential )
{
for ( int i = 0 ; i < 6 ; i ++)
{
_driveName = Path . GetFileNameWithoutExtension ( Path . GetRandomFileName ());
_cmdlet = cmdlet ;
// Need to get rid of the trailing \, otherwise New-PSDrive will not work...
if ( path . EndsWith ( "\\" , StringComparison . OrdinalIgnoreCase ) || path . EndsWith ( "/" , StringComparison . OrdinalIgnoreCase ))
{
path = path . Remove ( path . Length - 1 );
}
PSDriveInfo mappedDrive = cmdlet . SessionState . Drive . GetAtScope ( _driveName , "local" );
if ( mappedDrive != null )
{
if ( mappedDrive . Root . Equals ( path ))
{
return ;
}
// Remove the drive after 5 tries
if ( i < 5 )
{
continue ;
}
cmdlet . SessionState . Drive . Remove ( _driveName , true , "local" );
}
mappedDrive = new PSDriveInfo ( _driveName , cmdlet . SessionState . Internal . GetSingleProvider ( "FileSystem" ),
2018-12-31 20:44:16 +05:00
path , string . Empty , credential );
2016-03-30 14:20:52 -07:00
cmdlet . SessionState . Drive . New ( mappedDrive , "local" );
break ;
}
}
/// <summary>
2019-01-06 04:40:24 +05:00
/// Disposes the class.
2016-03-30 14:20:52 -07:00
/// </summary>
public void Dispose ()
{
PSDriveInfo mappedDrive = _cmdlet . SessionState . Drive . GetAtScope ( _driveName , "local" );
if ( mappedDrive != null )
{
_cmdlet . SessionState . Drive . Remove ( _driveName , true , "local" );
}
GC . SuppressFinalize ( this );
}
}
}