From addd2efaed355a594936018c7c87946963821131 Mon Sep 17 00:00:00 2001 From: NileshGhodekar Date: Sat, 23 May 2020 21:43:09 +0100 Subject: [PATCH] New function DateTimeUtcToLocalTimeFunction. New optional culture name parameter to DateTimeFormat function. Added support for WAL Lookup resolution for PowerShellUserName and PowerShellUserPassword properties of RunPowerShellScript activity. --- .gitignore | 1 - ChangeLog.md | 18 ++- src/Scripts/EncryptConnectionString.ps1 | 72 ++++++++++ src/Scripts/EncryptData.ps1 | 4 +- src/VersionInfo.cs | 4 +- .../ActivitySettings.Designer.cs | 8 +- .../ActivitySettings.resx | 6 +- .../Forms/RunPowerShellScriptForm.cs | 4 +- .../Activities/RunPowerShellScript.cs | 56 +++++++- .../Common/EventIdentifier.cs | 33 +++-- .../Common/ExpressionFunction.cs | 125 ++++++++++++++++-- 11 files changed, 291 insertions(+), 40 deletions(-) create mode 100644 src/Scripts/EncryptConnectionString.ps1 diff --git a/.gitignore b/.gitignore index 90f8277..be0e969 100644 --- a/.gitignore +++ b/.gitignore @@ -211,6 +211,5 @@ GeneratedArtifacts/ _Pvt_Extensions/ ModelManifest.xml /src/ReferencedAssemblies -/src/Scripts /src/SolutionOutput /src/WAL.snk diff --git a/ChangeLog.md b/ChangeLog.md index 40d6e8d..d374bcf 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -9,7 +9,19 @@ All notable changes to MIMWAL project will be documented in this file. The "Unre * Support for `[//Value]` lookups in Query definitions across rest of the activities. ------------ -### Version 2.19.0111.0 +### Version 2.20.0523.0 + +#### Added + +* New [IndexByValueFunction][IndexByValue] function +* New [CRFunction][CR] function +* New [DateTimeUtcToLocalTimeFunction][DateTimeUtcToLocalTime] function +* New optional culture name parameter to [DateTimeFormatFunction][DateTimeFormat] function +* Added support for WAL Lookup resolution for PowerShellUserName and PowerShellUserPassword properties of [Run PowerShell Script][RunPowerShellScript] activity. + +------------ + +### Version 2.19.0112.0 #### Changed @@ -225,6 +237,7 @@ To get the backward compatible behaviour, define the app setting GenerateUniqueV [CreateSqlParameterFunction]: https://github.com/Microsoft/MIMWAL/wiki/CreateSqlParameter-Function [CreateSqlParameter2Function]: https://github.com/Microsoft/MIMWAL/wiki/CreateSqlParameter2-Function [CRLFFunction]: https://github.com/Microsoft/MIMWAL/wiki/CRLF-Function +[CRFunction]: https://github.com/Microsoft/MIMWAL/wiki/CR-Function [DateTimeAddFunction]: https://github.com/Microsoft/MIMWAL/wiki/DateTimeAdd-Function [DateTimeFormatFunction]: https://github.com/Microsoft/MIMWAL/wiki/DateTimeFormat-Function [DateTimeFromFileTimeUTCFunction]: https://github.com/Microsoft/MIMWAL/wiki/DateTimeFromFileTimeUTC-Function @@ -232,6 +245,7 @@ To get the backward compatible behaviour, define the app setting GenerateUniqueV [DateTimeNowFunction]: https://github.com/Microsoft/MIMWAL/wiki/DateTimeNow-Function [DateTimeSubtractFunction]: https://github.com/Microsoft/MIMWAL/wiki/DateTimeSubtract-Function [DateTimeToFileTimeUTCFunction]: https://github.com/Microsoft/MIMWAL/wiki/DateTimeToFileTimeUTC-Function +[DateTimeUtcToLocalTimeFunction]: https://github.com/Microsoft/MIMWAL/wiki/DateTimeUtcToLocalTime-Function [DivideFunction]: https://github.com/Microsoft/MIMWAL/wiki/Divide-Function [EqFunction]: https://github.com/Microsoft/MIMWAL/wiki/Eq-Function [EscapeDNComponentFunction]: https://github.com/Microsoft/MIMWAL/wiki/EscapeDNComponent-Function @@ -244,6 +258,7 @@ To get the backward compatible behaviour, define the app setting GenerateUniqueV [GreaterThanFunction]: https://github.com/Microsoft/MIMWAL/wiki/GreaterThan-Function [IIFFunction]: https://github.com/Microsoft/MIMWAL/wiki/IIF-Function [InsertValuesFunction]: https://github.com/Microsoft/MIMWAL/wiki/InsertValues-Function +[IndexByValueFunction]: https://github.com/Microsoft/MIMWAL/wiki/IndexByValue-Function [IsPresentFunction]: https://github.com/Microsoft/MIMWAL/wiki/IsPresent-Function [LastFunction]: https://github.com/Microsoft/MIMWAL/wiki/Last-Function [LeftFunction]: https://github.com/Microsoft/MIMWAL/wiki/Left-Function @@ -288,3 +303,4 @@ To get the backward compatible behaviour, define the app setting GenerateUniqueV [WrapXPathFilterFunction]: https://github.com/Microsoft/MIMWAL/wiki/WrapXPathFilter-Function [MIMWalFunctionsTable]: https://github.com/Microsoft/MIMWAL/wiki/Functions-Table [GenerateUniqueValueActivity]: https://github.com/Microsoft/MIMWAL/wiki/Generate-Unique-Value-Activity +[RunPowerShellScriptActivity]: https://github.com/Microsoft/MIMWAL/wiki/Run-PowerShell-Script-Activity diff --git a/src/Scripts/EncryptConnectionString.ps1 b/src/Scripts/EncryptConnectionString.ps1 new file mode 100644 index 0000000..3c4dd15 --- /dev/null +++ b/src/Scripts/EncryptConnectionString.ps1 @@ -0,0 +1,72 @@ +<# + This script demonstrates how to encrypt connection strings used by WAL ExecutSql* functions. + + If a connection string contains SQL user's password information, it's highly recommended that you do not leave them unencrypted in the app config file. + + For more information, see: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-strings-and-configuration-files#encrypting-configuration-file-sections-using-protected-configuration + + NOTE: This script will need to to be run on each FIMService server. +#> + +param ( + [string] $sectionName = "connectionStrings", + [string] $dataProtectionProvider = "DataProtectionConfigurationProvider" +) + +$Error.Clear() + +#The System.Configuration assembly must be loaded +$configurationAssembly = "System.Configuration, Version=2.0.0.0, Culture=Neutral, PublicKeyToken=b03f5f7f11d50a3a" +[void] [Reflection.Assembly]::Load($configurationAssembly) + +function TestIsAdministrator +{ + $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() + (New-Object Security.Principal.WindowsPrincipal $currentUser).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) +} + +if(!(TestIsAdministrator)) +{ + throw $("Admin rights are required to run this script.") +} + +Write-Host "Encrypting configuration section: '$sectionName'.." + +$appService = "FIMService" +$appPath = [string](Get-WmiObject -Query "Select * from Win32_Service Where Name='$appService'").PathName + +if ($appPath -eq $null) +{ + Write-Error "Unable to find get application path for windows service '$appService'." + return +} +else +{ + $appPath = $appPath.Trim('"') +} + +Write-Host "The app config file path is: '$appPath'." + +$appConfig = [System.Configuration.ConfigurationManager]::OpenExeConfiguration($appPath) +$section = $appConfig.GetSection($sectionName) + +if (!$section.SectionInformation.IsProtected) +{ + $section.SectionInformation.ProtectSection($dataProtectionProvider); + $section.SectionInformation.ForceSave = [System.Boolean]::True; + $appConfig.Save([System.Configuration.ConfigurationSaveMode]::Modified); + + if ($Error.Count -eq 0) + { + Write-Host "Success!! Encrypted the config section '$sectionName' in the app config file '$appPath'." + } +} +else +{ + Write-Host "The config section '$sectionName' in the app config file '!$appPath' is already encrypted." +} + +if ($Error) +{ + Write-Host "There were errors executing the script." +} \ No newline at end of file diff --git a/src/Scripts/EncryptData.ps1 b/src/Scripts/EncryptData.ps1 index 64dadb0..f2cfb0c 100644 --- a/src/Scripts/EncryptData.ps1 +++ b/src/Scripts/EncryptData.ps1 @@ -11,14 +11,14 @@ Finding Assembly verion and PublicKeyToken gacutil.exe -l | findstr WorkflowActivityLibrary Creatinig a self signed certificate for MIMWAL (You can use a legacy CSP such as Microsoft Strong Cryptographic Provider as shown in the example below) - $cert = New-SelfSignedCertificate -DnsName "MIMWAL" -CertStoreLocation "cert:\LocalMachine\My" -Provider "Microsoft Strong Cryptographic Provider" + $cert = New-SelfSignedCertificate -DnsName "MIMWAL Encryption (Do Not Delete)" -CertStoreLocation "cert:\LocalMachine\My" -Provider "Microsoft Strong Cryptographic Provider" -NotAfter (Get-Date).AddYears(20) $cert.Thumbprint As of version v2.18.1110.0, only FIMService account needs read access to the private key of the MIMWAL certificate created above. #> $Error.Clear() -$walAssemblyVersion = "2.16.0710.0" +$walAssemblyVersion = "2.20.0523.0" $walAssemblyPublicKeyToken = "31bf3856ad364e35" $encryptionCertThumbprint = "9C697919FB2FB2D6324ADE42D5F8CB49E8778C08" # cert to be used for encryption (from the cert:\localmachine\my\ store). diff --git a/src/VersionInfo.cs b/src/VersionInfo.cs index 08c1e7e..08db16d 100644 --- a/src/VersionInfo.cs +++ b/src/VersionInfo.cs @@ -22,7 +22,7 @@ internal static class VersionInfo /// Build Number (MMDD) /// Revision (if any on the same day) /// - internal const string Version = "2.19.0112.0"; + internal const string Version = "2.20.0523.0"; /// /// File Version information for the assembly consists of the following four values: @@ -31,6 +31,6 @@ internal static class VersionInfo /// Build Number (MMDD) /// Revision (if any on the same day) /// - internal const string FileVersion = "2.19.0112.0"; + internal const string FileVersion = "2.20.0523.0"; } } \ No newline at end of file diff --git a/src/WorkflowActivityLibrary.UI/ActivitySettings.Designer.cs b/src/WorkflowActivityLibrary.UI/ActivitySettings.Designer.cs index ce64a28..5310d8d 100644 --- a/src/WorkflowActivityLibrary.UI/ActivitySettings.Designer.cs +++ b/src/WorkflowActivityLibrary.UI/ActivitySettings.Designer.cs @@ -19,7 +19,7 @@ namespace MicrosoftServices.IdentityManagement.WorkflowActivityLibrary.UI { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class ActivitySettings { @@ -970,7 +970,7 @@ internal static string PowerShellUser { } /// - /// Looks up a localized string similar to The PowerShell user must be specified in the domain\userName or UPN format when Impersonate PowerShell User option is selected.. + /// Looks up a localized string similar to The PowerShell user must be specified in the domain\userName or UPN format or must be a valid WAL Lookup expression when Impersonate PowerShell User option is selected.. /// internal static string PowerShellUserFormatValidationError { get { @@ -979,7 +979,7 @@ internal static string PowerShellUserFormatValidationError { } /// - /// Looks up a localized string similar to Specify the user to be used to construct PowerShell Credential object. When impersonating, the user name must be in the domain\userName or UPN format.. + /// Looks up a localized string similar to Specify the user to be used to construct PowerShell Credential object. When impersonating, the user name must be in the domain\userName or UPN format or must be a valid WAL Lookup expression.. /// internal static string PowerShellUserHelpText { get { @@ -997,7 +997,7 @@ internal static string PowerShellUserPassword { } /// - /// Looks up a localized string similar to Specify the password to be used to construct PowerShell Credential object. The expected format is: [base64EncodedEncryptedData] | [app:\appSettings\[key],[LocalMachine|CurrentUser]] | [cert:\[LocalMachine|CurrentUser]\my\[thumbprint],base64EncodedEncryptedData]]. + /// Looks up a localized string similar to Specify the password to be used to construct PowerShell Credential object. The expected format is: [base64EncodedEncryptedData] | [app:\appSettings\[key],[LocalMachine|CurrentUser]] | [cert:\[LocalMachine|CurrentUser]\my\[thumbprint],base64EncodedEncryptedData]]. It can also be a valid WAL Lookup expression returing data in any one of these formats.. /// internal static string PowerShellUserPasswordHelpText { get { diff --git a/src/WorkflowActivityLibrary.UI/ActivitySettings.resx b/src/WorkflowActivityLibrary.UI/ActivitySettings.resx index cdc2daf..a7d6cc4 100644 --- a/src/WorkflowActivityLibrary.UI/ActivitySettings.resx +++ b/src/WorkflowActivityLibrary.UI/ActivitySettings.resx @@ -658,19 +658,19 @@ PowerShell Script User - Specify the user to be used to construct PowerShell Credential object. When impersonating, the user name must be in the domain\userName or UPN format. + Specify the user to be used to construct PowerShell Credential object. When impersonating, the user name must be in the domain\userName or UPN format or must be a valid WAL Lookup expression. PowerShell Script User Password - Specify the password to be used to construct PowerShell Credential object. The expected format is: [base64EncodedEncryptedData] | [app:\appSettings\[key],[LocalMachine|CurrentUser]] | [cert:\[LocalMachine|CurrentUser]\my\[thumbprint],base64EncodedEncryptedData]] + Specify the password to be used to construct PowerShell Credential object. The expected format is: [base64EncodedEncryptedData] | [app:\appSettings\[key],[LocalMachine|CurrentUser]] | [cert:\[LocalMachine|CurrentUser]\my\[thumbprint],base64EncodedEncryptedData]]. It can also be a valid WAL Lookup expression returing data in any one of these formats. Specify UserName and Password of the impersonated user. - The PowerShell user must be specified in the domain\userName or UPN format when Impersonate PowerShell User option is selected. + The PowerShell user must be specified in the domain\userName or UPN format or must be a valid WAL Lookup expression when Impersonate PowerShell User option is selected. Authorization Policy cannot be applied when Request Actor is Service Account diff --git a/src/WorkflowActivityLibrary.UI/Forms/RunPowerShellScriptForm.cs b/src/WorkflowActivityLibrary.UI/Forms/RunPowerShellScriptForm.cs index 0642d36..b4544eb 100644 --- a/src/WorkflowActivityLibrary.UI/Forms/RunPowerShellScriptForm.cs +++ b/src/WorkflowActivityLibrary.UI/Forms/RunPowerShellScriptForm.cs @@ -554,7 +554,9 @@ public override bool ValidateInputs() if (!string.IsNullOrEmpty(this.powerShellUser.Value)) { - if (this.impersonatePowerShellUser.Value) + var powerShellUserIsExpression = evaluator.ParseIfExpression(this.powerShellUser.Value); + + if (this.impersonatePowerShellUser.Value && !powerShellUserIsExpression) { if (!this.powerShellUser.Value.Contains(@"\") && !this.powerShellUser.Value.Contains("@")) { diff --git a/src/WorkflowActivityLibrary/Activities/RunPowerShellScript.cs b/src/WorkflowActivityLibrary/Activities/RunPowerShellScript.cs index 69ac063..4403851 100644 --- a/src/WorkflowActivityLibrary/Activities/RunPowerShellScript.cs +++ b/src/WorkflowActivityLibrary/Activities/RunPowerShellScript.cs @@ -653,6 +653,16 @@ private void Prepare_ExecuteCode(object sender, EventArgs e) // If the activity is configured for conditional execution, parse the associated expression this.ActivityExpressionEvaluator.ParseIfExpression(this.ActivityExecutionCondition); + if (!string.IsNullOrEmpty(this.PowerShellUser)) + { + this.ActivityExpressionEvaluator.ParseIfExpression(this.PowerShellUser); + } + + if (!string.IsNullOrEmpty(this.PowerShellUserPassword)) + { + this.ActivityExpressionEvaluator.ParseIfExpression(this.PowerShellUserPassword); + } + if (this.InputType == PowerShellInputType.Arguments && this.Arguments != null && this.Arguments.Count > 0) @@ -1018,18 +1028,54 @@ private PSCredential GetPowerShellCredential() return null; } + var userName = this.PowerShellUser; + var userPassword = this.PowerShellUserPassword; + + if (ExpressionEvaluator.IsExpression(userName)) + { + try + { + object resolved = this.ActivityExpressionEvaluator.ResolveExpression(userName); + if (resolved != null) + { + userName = resolved.ToString(); + } + } + catch (WorkflowActivityLibraryException) + { + // This may happen if the design time username starts with a $ + // Do nothing. Any valid error should already be reported. + } + } + + if (ExpressionEvaluator.IsExpression(userPassword)) + { + try + { + object resolved = this.ActivityExpressionEvaluator.ResolveExpression(userPassword); + if (resolved != null) + { + userPassword = resolved.ToString(); + } + } + catch (WorkflowActivityLibraryException) + { + // Do nothing. Any valid error should already be reported. + } + } + if (this.ImpersonatePowerShellUser) { - string[] userParts = this.PowerShellUser.Split(new string[] { @"\" }, StringSplitOptions.RemoveEmptyEntries); - if (userParts.Length != 2 && !this.PowerShellUser.Contains("@")) + string[] userParts = userName.Split(new string[] { @"\" }, StringSplitOptions.RemoveEmptyEntries); + if (userParts.Length != 2 && !userName.Contains("@")) { - throw Logger.Instance.ReportError(EventIdentifier.RunPowerShellScriptRunScriptExecutionFailedError, new WorkflowActivityLibraryException(Messages.RunPowerShellActivity_InvalidUserFormat, this.PowerShellUser)); + throw Logger.Instance.ReportError(EventIdentifier.RunPowerShellScriptRunScriptExecutionFailedError, new WorkflowActivityLibraryException(Messages.RunPowerShellActivity_InvalidUserFormat, userName)); } } - SecureString password = ProtectedData.DecryptData(this.PowerShellUserPassword); + SecureString password = ProtectedData.DecryptData(userPassword); - return new PSCredential(this.PowerShellUser, password); + return new PSCredential(userName, password); } /// diff --git a/src/WorkflowActivityLibrary/Common/EventIdentifier.cs b/src/WorkflowActivityLibrary/Common/EventIdentifier.cs index 30e7b2c..c02a9e1 100644 --- a/src/WorkflowActivityLibrary/Common/EventIdentifier.cs +++ b/src/WorkflowActivityLibrary/Common/EventIdentifier.cs @@ -1296,7 +1296,12 @@ public static class EventIdentifier /// /// The event identifier for ExpressionFunction CR events /// - public const int ExpressionFunctionCr = 11688; + public const int ExpressionFunctionCR = 11688; + + /// + /// The event identifier for ExpressionFunction DateTimeUtcToLocalTime events + /// + public const int ExpressionFunctionDateTimeUtcToLocalTime = 11689; /// /// The event identifier for LookupEvaluator Constructor events @@ -2358,6 +2363,11 @@ public static class EventIdentifier /// public const int ExpressionFunctionDateTimeFormatInvalidFirstFunctionParameterTypeError = 41621; + /// + /// The event identifier for ExpressionFunction DateTimeFormat events + /// + public const int ExpressionFunctionDateTimeFormatInvalidThirdFunctionParameterTypeError = 41621; + /// /// The event identifier for ExpressionFunction DateTimeNow events /// @@ -2758,11 +2768,6 @@ public static class EventIdentifier /// public const int ExpressionFunctionCrlfInvalidFunctionParameterCountError = 41658; - /// - /// The event identifier for ExpressionFunction CR events - /// - public const int ExpressionFunctionCrInvalidFunctionParameterCountError = 41658; - /// /// The event identifier for ExpressionFunction EscapeDNComponent events /// @@ -3194,14 +3199,24 @@ public static class EventIdentifier public const int ExpressionFunctionModInvalidSecondFunctionParameterTypeError = 41686; /// - /// The event identifier for ExpressionFunction ValueByIndex events + /// The event identifier for ExpressionFunction IndexByValue events /// public const int ExpressionFunctionIndexByValueInvalidFunctionParameterCountError = 41687; /// - /// The event identifier for ExpressionFunction IndexByValue events + /// The event identifier for ExpressionFunction CR events + /// + public const int ExpressionFunctionCRInvalidFunctionParameterCountError = 41688; + + /// + /// The event identifier for ExpressionFunction DateTimeUtcToLocalTime events + /// + public const int ExpressionFunctionDateTimeUtcToLocalTimeInvalidFunctionParameterCountError = 41689; + + /// + /// The event identifier for ExpressionFunction DateTimeUtcToLocalTime events /// - public const int ExpressionFunctionIndexByValueNullFunctionParameterError = 41645; + public const int ExpressionFunctionDateTimeUtcToLocalTimeInvalidSecondFunctionParameterTypeError = 41689; /// /// The event identifier for LookupEvaluator Constructor events diff --git a/src/WorkflowActivityLibrary/Common/ExpressionFunction.cs b/src/WorkflowActivityLibrary/Common/ExpressionFunction.cs index 003cd15..86c7d3a 100644 --- a/src/WorkflowActivityLibrary/Common/ExpressionFunction.cs +++ b/src/WorkflowActivityLibrary/Common/ExpressionFunction.cs @@ -195,6 +195,9 @@ public object Run() case "DATETIMESUBTRACT": return this.DateTimeSubtract(); + case "DATETIMEUTCTOLOCALTIME": + return this.DateTimeUtcToLocalTime(); + case "DIVIDE": return this.Divide(); @@ -2220,7 +2223,7 @@ private object DateTimeAdd() /// /// This function is used to format the value of the first DateTime parameter in the format specified in the second string parameter. - /// Function Syntax: DateTimeFormat(date:DateTime, format:string) + /// Function Syntax: DateTimeFormat(date:DateTime, format:string [, culture:name]) /// /// The string representation of the first DateTime parameter in the format specified in the second string parameter. private string DateTimeFormat() @@ -2229,9 +2232,9 @@ private string DateTimeFormat() try { - if (this.parameters.Count != 2) + if (this.parameters.Count < 2 || this.parameters.Count > 3) { - throw Logger.Instance.ReportError(EventIdentifier.ExpressionFunctionDateTimeFormatInvalidFunctionParameterCountError, new InvalidFunctionFormatException(Messages.ExpressionFunction_InvalidFunctionParameterCountError, this.function, 2, this.parameters.Count)); + throw Logger.Instance.ReportError(EventIdentifier.ExpressionFunctionDateTimeFormatInvalidFunctionParameterCountError, new InvalidFunctionFormatException(Messages.ExpressionFunction_InvalidFunctionParameterCountError2, this.function, 2, 3, this.parameters.Count)); } if (this.parameters[1] == null) @@ -2255,9 +2258,29 @@ private string DateTimeFormat() } else { - result = ((DateTime)this.parameters[0]).ToString(this.parameters[1].ToString(), CultureInfo.InvariantCulture); + var cultureInfo = CultureInfo.InvariantCulture; + if (this.parameters.Count == 3) + { + try + { + cultureInfo = new CultureInfo(this.parameters[2] as string); + } + catch (ArgumentException e) + { + throw Logger.Instance.ReportError(EventIdentifier.ExpressionFunctionDateTimeFormatInvalidThirdFunctionParameterTypeError, e); + } + } + + result = ((DateTime)this.parameters[0]).ToString(this.parameters[1].ToString(), cultureInfo); - Logger.Instance.WriteVerbose(EventIdentifier.ExpressionFunctionDateTimeFormat, "DateTimeFormat('{0}', '{1}') returned '{2}'.", this.parameters[0], this.parameters[1], result); + if (this.parameters.Count == 2) + { + Logger.Instance.WriteVerbose(EventIdentifier.ExpressionFunctionDateTimeFormat, "DateTimeFormat('{0}', '{1}') returned '{2}'.", this.parameters[0], this.parameters[1], result); + } + else + { + Logger.Instance.WriteVerbose(EventIdentifier.ExpressionFunctionDateTimeFormat, "DateTimeFormat('{0}', '{1}', '{2}') returned '{3}'.", this.parameters[0], this.parameters[1], this.parameters[2], result); + } } } else @@ -2476,6 +2499,84 @@ private object DateTimeToFileTimeUtc() } } + /// + /// This function is used to convert a date to the local time or specifed time zone. + /// Function Syntax: DateTimeUtcToLocalTime(date:DateTime [, TimeZoneId]) + /// + /// The value of the specified UTC date expressed in the local time or specified time zone. + private object DateTimeUtcToLocalTime() + { + Logger.Instance.WriteMethodEntry(EventIdentifier.ExpressionFunctionDateTimeUtcToLocalTime, "Evaluation Mode: '{0}'.", this.mode); + + try + { + if (this.parameters.Count < 1 || this.parameters.Count > 2) + { + throw Logger.Instance.ReportError(EventIdentifier.ExpressionFunctionDateTimeUtcToLocalTimeInvalidFunctionParameterCountError, new InvalidFunctionFormatException(Messages.ExpressionFunction_InvalidFunctionParameterCountError2, this.function, 1, 2, this.parameters.Count)); + } + + Type parameterType = typeof(DateTime); + object parameter = this.parameters[0]; + if (!this.VerifyType(parameter, parameterType)) + { + throw Logger.Instance.ReportError(EventIdentifier.ExpressionFunctionDateTimeToFileTimeUtcInvalidFirstFunctionParameterTypeError, new InvalidFunctionFormatException(Messages.ExpressionFunction_InvalidFirstFunctionParameterTypeError, this.function, parameterType.Name, parameter == null ? "null" : parameter.GetType().Name)); + } + + object result; + if (this.mode != EvaluationMode.Parse) + { + if (this.parameters[0] == null) + { + result = null; + } + else + { + if (this.parameters.Count == 2) + { + var timeZoneId = this.parameters[1] as string; + if (string.IsNullOrEmpty(timeZoneId)) + { + result = ((DateTime)this.parameters[0]).ToLocalTime(); + } + else + { + try + { + TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); + result = TimeZoneInfo.ConvertTimeFromUtc((DateTime)this.parameters[0], timeZone); + } + catch (TimeZoneNotFoundException e) + { + throw Logger.Instance.ReportError(EventIdentifier.ExpressionFunctionDateTimeUtcToLocalTimeInvalidSecondFunctionParameterTypeError, e); + } + catch (InvalidTimeZoneException e) + { + throw Logger.Instance.ReportError(EventIdentifier.ExpressionFunctionDateTimeUtcToLocalTimeInvalidSecondFunctionParameterTypeError, e); + } + } + + Logger.Instance.WriteVerbose(EventIdentifier.ExpressionFunctionDateTimeUtcToLocalTime, "DateTimeUtcToLocalTime('{0}', '{1}') returned '{2}'.", this.parameters[0], this.parameters[1], result); + } + else + { + result = ((DateTime)this.parameters[0]).ToLocalTime(); + Logger.Instance.WriteVerbose(EventIdentifier.ExpressionFunctionDateTimeUtcToLocalTime, "DateTimeUtcToLocalTime('{0}') returned '{1}'.", this.parameters[0], result); + } + } + } + else + { + result = null; + } + + return result; + } + finally + { + Logger.Instance.WriteMethodExit(EventIdentifier.ExpressionFunctionDateTimeUtcToLocalTime, "Evaluation Mode: '{0}'.", this.mode); + } + } + /// /// This function is used to get the timespan between the two dates. /// Function Syntax: DateTimeSubtract(date:DateTimeEndDate, date:DateTimeStartDate) @@ -2701,8 +2802,8 @@ private object IIF() /// This function is used to retrieve the index for a specific value within the input list. /// Function Syntax: IndexByValue(values:[list or object], value:object) /// - /// The index of the specified value in the input list. If the value is not found in the list, null is returned. - private object IndexByValue() + /// The index of the specified value in the input list. If the value is null or not found in the list, -1 is returned. + private int IndexByValue() { Logger.Instance.WriteMethodEntry(EventIdentifier.ExpressionFunctionIndexByValue, "Evaluation Mode: '{0}'.", this.mode); @@ -2730,7 +2831,7 @@ private object IndexByValue() { if (item is string && this.parameters[1] is string) { - if (item.ToString().Equals(this.parameters[1].ToString(), StringComparison.InvariantCultureIgnoreCase)) + if (item.ToString().Equals(this.parameters[1].ToString(), StringComparison.OrdinalIgnoreCase)) { Logger.Instance.WriteVerbose(EventIdentifier.ExpressionFunctionIndexByValue, "IndexByValue('{0}', '{1}') Matched string item '{2}' to second param '{3}'. Result: {4}.", this.parameters[0], this.parameters[1], item.ToString(), this.parameters[1].ToString(), index); result = index; @@ -4907,13 +5008,13 @@ private string ConvertToString() /// A CR is the output. private string CR() { - Logger.Instance.WriteMethodEntry(EventIdentifier.ExpressionFunctionCr, "Evaluation Mode: '{0}'.", this.mode); + Logger.Instance.WriteMethodEntry(EventIdentifier.ExpressionFunctionCR, "Evaluation Mode: '{0}'.", this.mode); try { if (this.parameters.Count != 0) { - throw Logger.Instance.ReportError(EventIdentifier.ExpressionFunctionCrInvalidFunctionParameterCountError, new InvalidFunctionFormatException(Messages.ExpressionFunction_InvalidFunctionParameterCountError, this.function, 0, this.parameters.Count)); + throw Logger.Instance.ReportError(EventIdentifier.ExpressionFunctionCRInvalidFunctionParameterCountError, new InvalidFunctionFormatException(Messages.ExpressionFunction_InvalidFunctionParameterCountError, this.function, 0, this.parameters.Count)); } string result; @@ -4921,7 +5022,7 @@ private string CR() if (this.mode != EvaluationMode.Parse) { result = "\n"; - Logger.Instance.WriteVerbose(EventIdentifier.ExpressionFunctionCr, "CR() returned '{0}'.", result); + Logger.Instance.WriteVerbose(EventIdentifier.ExpressionFunctionCR, "CR() returned '{0}'.", result); } else { @@ -4932,7 +5033,7 @@ private string CR() } finally { - Logger.Instance.WriteMethodExit(EventIdentifier.ExpressionFunctionCr, "Evaluation Mode: '{0}'.", this.mode); + Logger.Instance.WriteMethodExit(EventIdentifier.ExpressionFunctionCR, "Evaluation Mode: '{0}'.", this.mode); } }