Module.psm1
#Requires -Version 5.1 $ErrorActionPreference = "Stop" # Only suported on Windows PowerShell. For the time being. if ($IsCoreCLR) { Throw "Due to limitations with the Selenium PowerShell Module. This module is not supported on PowerShell Core yet." } Write-Host "Selenium MVP Module Loaded. Use 'ConnectTo-MVPPortal' to get started." -ForegroundColor Green #CompiledByBuildScript #Resource: 00_HTMLElements.ps1 Data LocalizedDataHTMLElements { ConvertFrom-StringData @' Date=DateOfActivity Title=TitleOfActivity URL=ReferenceUrl Description=Description AnnualQuantity=AnnualQuantity SecondAnnualQuantity=SecondAnnualQuantity AnnualReach=AnnualReach '@ } #Resource: 01_HTMLFormStrucuture.ps1 # Read-Only New-Variable -Name HTMLFormStructure -Scope Global -Option ReadOnly -Force -Value @( @{ Name = "Default" Properties = @( @{ Name = 'Date' Element = $LocalizedDataHTMLElements.Date isRequired = $true } @{ Name = 'Title' Element = $LocalizedDataHTMLElements.Title isRequired = $true } @{ Name = 'URL' Element = $LocalizedDataHTMLElements.URL isRequired = $false } @{ Name = 'Description' Element = $LocalizedDataHTMLElements.Description } ) } @{ Name = "Article" Properties = @( @{ Name = 'Number of Articles' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true }, @{ Name = 'Number of Views' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Blog/WebSite Post" Properties = @( @{ Name = 'URL' Element = $LocalizedDataHTMLElements.URL isRequired = $true } @{ Name = 'Number of Posts' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true }, @{ Name = 'Number of Subscribers' Element = $LocalizedDataHTMLElements.SecondAnnualQuantity }, @{ Name = 'Annual Unique Visitors' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Book (Author)" Properties = @( @{ Name = 'Number of Books' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true }, @{ Name = 'Copies Sold' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Book (Co-Author)" Properties = @( @{ Name = 'Number of Books' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true }, @{ Name = 'Copies Sold' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Conference (Staffing)" Properties = @( @{ Name = 'Number of Conferences' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true }, @{ Name = 'Number of Visitors' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Docs.Microsoft.com Contribution" Properties = @( @{ Name = 'Pull Requests/Issues/Submissions' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } ) } @{ Name = "Forum Moderator" Properties = @( @{ Name = 'Number of Threads moderated' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } ) } @{ Name = "Forum Participation (3rd Party forums)" Properties = @( @{ Name = 'URL' Element = $LocalizedDataHTMLElements.URL isRequired = $true } @{ Name = 'Number of Answers' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true }, @{ Name = 'Number of Posts' Element = $LocalizedDataHTMLElements.SecondAnnualQuantity isRequired = $true }, @{ Name = 'Views of answers' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Forum Participation (Microsoft Forums)" Properties = @( @{ Name = 'URL' Element = $LocalizedDataHTMLElements.URL isRequired = $true }, @{ Name = 'Number of Answers' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true }, @{ Name = 'Number of Posts' Element = $LocalizedDataHTMLElements.SecondAnnualQuantity }, @{ Name = 'Views of answers' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Mentorship" Properties = @( @{ Name = 'Mentoring Topic' Element = $LocalizedDataHTMLElements.Description } @{ Name = 'Number of Mentorship Activity' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } @{ Name = 'Number of Mentees' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Microsoft Open Source Projects" Properties = @( @{ Name = 'Number of Projects' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } ) } @{ Name = "Non-Microsoft Open Source Projects" Properties = @( @{ Name = 'Projects(s)' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } @{ Name = 'Contribution(s)' Element = $LocalizedDataHTMLElements.SecondAnnualQuantity } ) } @{ Name = "Organizer (User Group/Meetup/Local Events)" Properties = @( @{ Name = 'Meetings' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } @{ Name = 'Members' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Organizer of Conference" Properties = @( @{ Name = 'Number of Conferences' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } @{ Name = 'Number of Attendees' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Other" Properties = @( @{ Name = 'Annual quantity' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } @{ Name = 'Annual reach' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Product Group Feedback" Properties = @( @{ Name = 'Number of Events Participated' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } @{ Name = 'Number of Feedbacks Provided' Element = $LocalizedDataHTMLElements.SecondAnnualQuantity } ) } @{ Name = "Sample Code/Projects/Tools" Properties = @( @{ Name = 'URL' Element = $LocalizedDataHTMLElements.URL isRequired = $true } @{ Name = 'Number of Samples' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } @{ Name = 'Number of Downloads' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Site Owner" Properties = @( @{ Name = 'Number of Sites' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } @{ Name = 'Number of Visitors' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Speaking (Conference)" Properties = @( @{ Name = 'Number of Talks' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } @{ Name = 'Attendees of Talks' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Speaking (User Group/Meetup/Local events)" Properties = @( @{ Name = 'Number of Talks' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } @{ Name = 'Attendees of Talks' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Technical Social Media (Twitter, Facebook, LinkedIn...)" Properties = @( @{ Name = 'URL' Element = $LocalizedDataHTMLElements.URL isRequired = $true } @{ Name = 'Number of Talks' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } @{ Name = 'Number of Followers' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Translation Review, Feedback and Editing" Properties = @( @{ Name = 'Annual quantity' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } ) } @{ Name = "Video/Webcast/Podcast" Properties = @( @{ Name = 'URL' Element = $LocalizedDataHTMLElements.URL isRequired = $true } @{ Name = 'Number of Videos' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } @{ Name = 'Number of Views' Element = $LocalizedDataHTMLElements.AnnualReach } ) } @{ Name = "Workshop/Volunteer/Proctor" Properties = @( @{ Name = 'Number of Events' Element = $LocalizedDataHTMLElements.AnnualQuantity isRequired = $true } ) } ) #Resource: 02_HTMLFormStructureFormatting.ps1 New-Variable -Name HTMLFormStructureValidate -Scope Global -Option ReadOnly -Force -Value @( @{ Name = "Date" Format = { param($date) $currentCultureDate = [DateTime]::Parse($date, [cultureinfo]::New((Get-Culture).Name)) write-output ($currentCultureDate.ToString("MM/dd/yyyy")) } } @{ Name = "URL" Format = { param($url) Write-Output ([URI]::New($url).AbsoluteUri) } } ) #Resource: 03_LocalizedData.ps1 data LocalizedData { ConvertFrom-StringData @' ErrorConnectToMVPPortal=An Error occured when attempting to connect to the portal. Details: '{0}' ErrorNestedMVPActivity=Nested MVPActivity. MVPActivity is only requried for the top-level declaration. Refer to usage. ErrorMissingMVPActivityArea=Missing 'Area' Statement. ErrorMissingMVPActivityAreaMultiple=Multiple Statements of 'Area' was detected. Only a single instance is permitted. ErrorMissingMVPActivityContributionArea=Missing 'ContributionArea' Statement. ErrorExceedMVPActivityContributionArea=Exceeded 'ContributionArea' Limit. Limit is 2. Count was '{0}'. ErrorMissingMVPActivityValue=Missing 'Value' Statement. ErrorMissingActivityType=Unable to enumerate ActivityTypes from source HTML. ErrorMissingContributionType=Unable to enumerate ContributionAreas from source HTML. ErrorMissingDriver=Missing Selinum Driver. Use: ConnectTo-Selenium to connect. ErrorTryTentitiveCommand=Try-TentativeCommand: Exceeded Retry Limit. ErrorMissingSelectedValue=The Parameter '-SelectedValue' value '{0}', could not be found ErrorTooManySelectedValue=The Parameter '-SelectedValue' value '{0}', returned too many items. Count '{1}' ErrorJavaScriptTimeout=Javascript Timeout. ErrorNoActivityButton=Cannot Select Add New Activity Button. ErrorAreaNotNested=Error. 'Area' is not nested within MVPActivity. ErrorContributionAreaNotNested=Error. 'ContributionArea' is not nested within MVPActivity. ErrorHTMLFormStructureDefaultParameter=Error. The 'default' Parameter Value is prohibited. ErrorHTMLFormStructureMissingName=Error. Could not match name '{0}' with HTMLFormStructure. ErrorCannotFindHTMLElement=Error. Could not match HTML element '{0}' with HTMLFormStructure. ErrorTooManyHTMLElements=Error. Too many results with HTML element '{0} with HTMLFormStructure. Count '{1}' ErrorFieldValidationError=Error. A field validation error was found within the form. Error: '{0}'" ErrorMissingRequiredEntries=Error. Missing Required HTML Fields: '{0}' ErrorMissingMVPActivityInCallStack=Error. '{0}' can only be called from within MVPActivity. ErrorFormattingValue=Error. There was an error raised when attempting to format '{0}' with '{1}'. Error Message: {2} ErrorSavingMVPActivity=Error. There was an error raised when attempting to save the MVP activity. Error: '{0}' ErrorSelectingDropDown=Error. There was an error raised when attempting to select the dropDown '{0}' with the value: '{1}'. Error: '{2}' ErrorStopMVPActivity=Error. There was an error raised when attempting to cancel the MVP activity. Error: '{0}' ErrorAreaFailure=Error. Could not set MVP Area within HTML Form, unable to continue. ErrorTestCSVSchemaMissingCSVFile="Missing CSV File: '{0}'" ErrorTestCSVSchemaImportCSVFile=An error occured when importing the CSV File: Error '{0}' ErrorTestCSVSchemaMissingColumns=Error. Validation Failed: Missing Columns '{0}' ErrorTestCSVSchemaDifferentAreaColumn=Formatting Error. Validation Failed: Cannot have different Areas ('{0}') within the same CSV File. ErrorTestCSVSchemaDuplicateContributionArea=Duplicate Contribution Area Found with SecondContributionArea and ThirdContributionArea columns within the CSV File. Please correct this issue and try again! ErrorParseValueCheck=Parser Check Failed. Please ensure that 'Value' has the correct names added that are appropriate for the respective 'Area'. Please run "Get-AreaNamedValues -AreaName '{0}'" to get correct 'Value' names. Alternatively, you can run: 'New-MVPFixture -AreaName '{0}' to generate an example template. ErrorParseContributionAreaCheck=Parser Check Failed. Please ensure that 'ContributionArea' has the correct names added. Please run "Get-Help ContributionArea" for a list of valid Contribution Areas. Error500=Error 500 Detected on the MVP Portal. This is an intermittent error. WarningEntryWasNotSaved=An Error occured when attempting to add the entry. THE ENTRY WAS NOT SAVED. ElementIdActivityType=activityTypeSelector ElementIdContributionArea=select_contributionAreasDDL ElementButtonNewActivity=addNewActivityBtn ElementButtonCancelActivity=submitCloseButton ElementButtonSubmitActivity=submitActivityButton ElementValueArticle=e36464de-179a-e411-bbc8-6c3be5a82b68 ElementValueChefPuppetInDataCenter=b803f4ef-066b-e511-810b-fc15b428ced0 ElementFieldValidationError=field-validation-error ElementVisibilityBoxXPath=//a[contains(@class,'Txt') and contains(@role,'navigation')] VisibilityListItem=//li[contains(@class,'Item') and contains(@txt,'{0}')] VariableSaveActivitySleepCounter=SaveActivitySleepCounter TestActivityRegexMVPActivity=^MVPActivity TestActivityRegexMVPArea=^Area TestActivityRegexMVPContributionArea=^ContributionArea TestActivityRegexMVPVisibility=^Visibility TestActivityRegexMVPValue=^Value ConnectToMVPPortalRegexURLMatch=^https:\/\/login\.((microsoftonline)|(live))\.com MVPPortal500Error=^https:\/\/mvp\.microsoft\.com\/Error\/500\?.+ '@ } #Resource: 04_HTMLContributionAreas.ps1 # Read-Only New-Variable -Name HTMLContributionAreas -Scope Global -Option ReadOnly -Force -Value @( 'Chef/Puppet in Datacenter' 'Container Management' 'Datacenter Management' 'Group Policy' 'High Availability' 'Hyper-V' 'Linux in System Center/Operations Management Suite' 'Linux on Hyper-V' 'Networking' 'PowerShell' 'Storage' 'Windows Server for Small & Medium Business' 'Azure Bot Service' 'Azure Cognitive Search' 'Azure Cognitive Services' 'Azure Machine Learning' 'Dynamics 365' 'Power Apps' 'Power Automate' 'Power Pages' 'Power Virtual Agents' 'Azure Arc Enabled Data Services' 'Azure Cosmos DB' 'Azure Data Catalog' 'Azure Data Explorer' 'Azure Data Lake' 'Azure Database for MySQL' 'Azure Database for PostgreSQL' 'Azure Databricks' 'Azure HDInsight and Hadoop & Spark on Azure' 'Azure Search' 'Azure SQL (Database, Pools, Serverless, Hyperscale, Managed Instance, Virtual Machines)' 'Azure SQL Edge' 'Azure Stream Analytics' 'Azure Synapse Analytics' 'Big Data Clusters' 'Cortana Intelligence Suite' 'Data Warehousing (Azure SQL Data Warehouse, Fast Track & APS)' 'Information Management (ADF, SSIS, & Data Sync)' 'Microsoft Purview' 'Power BI' 'SQL Server (on Windows, Linux, Containers)' 'SQL Server Machine Learning Services (R, Python)' 'SQL Server Reporting Services & Analysis Services' 'Tools & Connectivity' '.NET' 'Accessibility' 'ASP.NET/IIS' 'C++' 'Developer Security' 'Front End Web Dev' 'Github & Azure DevOps' 'Java' 'Javascript/Typescript' 'Node.js' 'Python' 'Quantum' 'Unity' 'Visual Studio Code' 'Visual Studio Extensibility' 'Xamarin' 'Information Protection' 'Microsoft Intune' 'Previous Expertise: Directory Services' 'Remote Desktop Services' 'Azure Edge Devices' 'Azure IoT Services & Development' 'Access' 'Excel' 'Exchange' 'Microsoft Stream' 'Microsoft Teams' 'Microsoft Viva' 'Office 365' 'OneDrive' 'OneNote' 'Outlook' 'PowerPoint' 'Project' 'SharePoint' 'Skype for Business' 'Visio' 'Word' 'Yammer' 'Microsoft Graph' 'Microsoft Teams Development' 'Outlook Development' 'SharePoint Development' 'W/X/P Development' 'ARM Templates (Infra as Code)' 'Azure API Management' 'Azure App Service' 'Azure Arc Enabled Infrastructure' 'Azure Backup & Disaster Recovery' 'Azure Blockchain' 'Azure Core Compute (VMSS, Confidential Computing, Platform Deployment)' 'Azure Cost Management' 'Azure Functions' 'Azure Hybrid' 'Azure Kubernetes, Container Instances, Docker' 'Azure Lighthouse' 'Azure Logic Apps' 'Azure Migrate' 'Azure Monitor' 'Azure Networking' 'Azure Policy & Governance' 'Azure SDK (Software Development Kit) and CLIs ( Az CLI, PowerShell, Terraform, Ansible)' 'Azure Service Fabric' 'Azure Storage' 'Distributed App Runtime (Dapr), Open App Model (OAM), Open Service Mesh (OSM)' 'Previous Expertise: Microsoft Azure' 'Service Bus, Event Hubs, Event Grid, Relay' 'D365 Mixed Reality' 'MR Design' 'MR Development' 'Cloud Security' 'Identity & Access' 'SIEM & XDR' 'Surface for IT' 'Windows 365' 'Windows for IT' 'Windows Design' 'Windows Development' ) #BuildFileName: Get-ActivityTypes.ps1 Function Get-ActivityTypes { [CmdletBinding()] param() Write-Verbose "Get-ActivityType Called:" Test-SEDriver # If the Activity Types have already been cached, then return the cache if (Test-SEActivityTypes) { Write-Verbose "[Get-ActivityType] Returning Cache:" return $Global:SEActivityTypes } # Load and Parse the HTML $HTMLDoc = [HtmlAgilityPack.HtmlDocument]::new() $HTMLDoc.LoadHtml($Global:MVPDriver.PageSource) # Retrive the Contribution Areas $ActivityTypes = $HTMLDoc.GetElementbyId("activityTypeSelector").ChildNodes.Where{$_.Name -eq 'option'} if ($ActivityTypes.count -eq 0) { $PSCmdlet.ThrowTerminatingError($LocalizedData.ErrorMissingActivityType)} # Build Custom Object $Global:SEActivityTypes = ($ActivityTypes | Select-Object @{ Name="Name" Expression={$_.InnerText}}, @{ Name="Value" Expression={($_.Attributes.Where{$_.Name -eq 'value'}).Value} }) Write-Debug ("[Get-ActivityType] Global:SEActivityTypes: {0}" -f ($Global:SEActivityTypes | ConvertTo-Json)) Write-Output $Global:SEActivityTypes } #BuildFileName: Get-ASTInstanceValues.ps1 function Get-ASTInstanceValues { [CmdletBinding()] param( [Parameter(Mandatory)] [System.Management.Automation.Language.CommandAst[]] $InstanceValues, [Parameter()] [string] $ParameterName ) $values = [System.Collections.Generic.List[String]]::New() # Split Instances with ParameterNames and Without ParameterNames $InstanceValuesWithParameterNames,$InstanceValuesWithoutParameterNames = $InstanceValues.Where({ $_.CommandElements.Where{$_.ParameterName} }, 'Split') # Return a list of cmdlet's that have parsed parameters into the input and capture their values forEach ($valueInstance in $InstanceValuesWithParameterNames) { $Result = 0..$valueInstance.CommandElements.count | Where-Object { ($valueInstance.CommandElements[$_].ParameterName -eq $ParameterName) } # Get the following item in the array, which will be the value of the parameter name $values.Add($valueInstance.CommandElements[$Result+1].Value) } $InstanceValuesWithoutParameterNames | ForEach-Object { $values.Add( $_.CommandElements[1].Value ) } return $values } #BuildFileName: Get-ContributionAreas.ps1 Function Get-ContributionAreas { [CmdletBinding()] param() Write-Verbose "Get-ContributionAreas Called:" Test-SEDriver # If the Activity Types have already been cached, then return the cache if (Test-SEContributionAreas) { Write-Verbose "[Get-ContributionAreas] Returning Cache:" return $Global:SEContributionAreas } # Load and Parse the HTML $HTMLDoc = [HtmlAgilityPack.HtmlDocument]::new() $HTMLDoc.LoadHtml($Global:MVPDriver.PageSource) # Retrive the Contribution Areas $ContributionAreas = $HTMLDoc.GetElementbyId("select_contributionAreasDDL").ChildNodes.Where{$_.Name -eq "optgroup"} # Filter by Non-Disabled Attributes. Disabled Attributes are headers $OptionElements = $ContributionAreas.ChildNodes.Where{$_.Attributes.Name -notcontains 'disabled'} if ($OptionElements.count -eq 0) { $PSCmdlet.ThrowTerminatingError($LocalizedData.ErrorMissingContributionType)} # HTML Sanitization $htmlSanitizer = { param($value) $value = $value -replace '&', '&' $value = $value -replace ' ', '' Write-Output $value.trim() } # Build Custom Object $Global:SEContributionAreas = ($OptionElements | Select-Object @{ Name="Name" Expression={ $htmlSanitizer.Invoke($_.InnerText) } }, @{ Name="Value" Expression={($_.Attributes.Where{$_.Name -eq 'data-contributionid'}).Value} }) Write-Debug ("[Get-ContributionAreas] Global:SEContributionAreas: {0}" -f ($Global:SEActivityTypes | ConvertTo-Json)) Write-Output $Global:SEContributionAreas } #BuildFileName: Get-HTMLFormStructure.ps1 function Get-HTMLFormStructure { [CmdletBinding()] param ( # Parameter help description [Parameter(Mandatory)] [String] $Name ) Write-Verbose "Get-HTMLFormStructure Started:" # Trim the input so that we can be more forgiving. $Name = $Name.Trim() $output = [System.Collections.Generic.List[PSCustomObject]]::New() # The Default Parameter Value is Prohibited if ($Name -eq "Default") { Throw $LocalizedData.ErrorHTMLFormStructureDefaultParameter } $defaultProperties = $Global:HTMLFormStructure.Where{$_.Name -eq 'Default'} [Array]$matched = $Global:HTMLFormStructure.Where{($_.Name -eq $Name) -or ($_.Element -eq $Name)} # If the Either The Element of the String name can't be found, # then Throw a terminating error. if ($matched.count -eq 0) { Throw ($LocalizedData.ErrorHTMLFormStructureMissingName -f $Name) } # If there is more then 1 result, data duplicate. Throw temrinating error. if ($matched.count -ne 1) { Throw ($LocalizedData.ErrorHTMLFormStructureMissingName -f $Name) } # Join the Default data with the matched data forEach ($Property in $defaultProperties.Properties) { # Test if the Element Already Exists. If so, skip. if ($matched.Properties.Element -contains $Property.Element) { Write-Debug ("[Get-HTMLFormStructure] Duplicate Element '{0}'" -f $Property.Element) continue } $obj = [PSCustomObject]@{ Name = $Property.Name Element = $Property.Element isRequired = $( if ($Property.Keys -notcontains "isRequired") { $false } else { $Property.isRequired } ) isSet = $false } Write-Debug ("[Get-HTMLFormStructure] Adding Default Element '{0}' " -f ($obj | ConvertTo-Json)) $output.Add($obj) } forEach ($Property in $matched.properties) { $obj = [PSCustomObject]@{ Name = $Property.Name Element = $Property.Element isRequired = $( if ($Property.Keys -notcontains "isRequired") { $false } else { $Property.isRequired } ) isSet = $false } Write-Debug ("[Get-HTMLFormStructure] Adding Element '{0}' " -f ($obj | ConvertTo-Json)) $output.Add($obj) } # # Join the Formatting Scriptblock form the HTMLFormStructureValidate Variable. $output = $output | Select-Object *, @{ Name = "Format" Expression = { $Name = $_.Name try { ($Global:HTMLFormStructureValidate.Where{$_.Name -eq $Name}).Format } catch {} } } Write-Output $output Write-Verbose "Get-HTMLFormStructure Completed:" } #BuildFileName: Invoke-MVPActivity.ps1 function Invoke-MVPActivity { [CmdletBinding()] param() # Update Streams Write-Verbose "Invoke-MVPActivity:" # Update Debug Stream Write-Debug "[Invoke-MVPActivity] Calling Test-SEDriver:" # Test if the Driver is active. If not throw a terminating error. Test-SEDriver # Update Debug Stream Write-Debug "[Invoke-MVPActivity] Calling Try-TentativeCommand:" Write-Debug ("[Invoke-MVPActivity] Try-TentativeCommand Params: {0}" -f ($params | ConvertTo-Json)) $result = ttry { # Try and click "Add New Activity" $ActivityButton = Find-SeElement -Driver $Global:MVPDriver -Id $LocalizedData.ElementButtonNewActivity Invoke-SeClick -Element $ActivityButton # Update Debug Write-Debug "[Invoke-MVPActivity:] Tentative-Try: Success" Write-Output $true } -catch { # Update Debug Write-Debug ("[Invoke-MVPActivity] Tentative-Try: Error Raised {0}" -f $_.Exception) # Try and close the activity if the window is already open. Stop-MVPActivity # Sleep for a second Start-Sleep -Seconds 1 } -RetryLimit 4 if ($null -eq $result) { # Update Verbose Stream Write-Verbose "Invoke-MVPActivity: Failure" Throw $LocalizedData.ErrorNoActivityButton } # Update Verbose Stream Write-Verbose "Invoke-MVPActivity: Success" Start-Sleep -Seconds 1 } #BuildFileName: New-CSVArguments.ps1 function New-CSVArguments { param( [Parameter(Mandatory)] [String] $LiteralPath ) # Import the CSV File $CSV = Import-Csv @PSBoundParameters # Get the CSV Column Names, excluding 'Area' & ContributionArea $CSVColumnNames = ($CSV | Get-Member -MemberType NoteProperty).Name # Get the HTMLForm Structure and Get the Column Names Match from the CSV File $FormStructure = Get-HTMLFormStructure @($CSV.Area)[0] | Where-Object { $_.Name -in $CSVColumnNames } $ArgumentList = $CSV | ForEach-Object { $row = $_ $params = @{ Area = $row.Area ContributionArea = ($row.ContributionArea, $row.SecondContributionArea, $row.ThirdContributionArea | Where-Object {$null -ne $_}) } if ($CSVColumnNames -contains 'Visibility') { $params.Visibility = $row.Visibility } # Iterate Through the Matched Items and Add them $FormStructure | ForEach-Object { $params.('{0}' -f $_.Name) = $row."$($_.Name)" } Write-Output $params } Write-Output $ArgumentList } #BuildFileName: New-CSVFixture.ps1 Function New-CSVFixture { param( [Parameter(Mandatory)] [String] $LiteralPath ) # Import the CSV File [Array]$CSV = Import-Csv @PSBoundParameters # Single CSV Entry if ($CSV.Count -eq 1) { $Area = $CSV.Area } else { $Area = $CSV.Area[0] } # Get the CSV Column Names, excluding 'Area' & ContributionArea $CSVColumnNames = ($CSV | Get-Member -MemberType NoteProperty).Name # Get the HTMLForm Structure and Get the Column Names Match from the CSV File $FormStructure = Get-HTMLFormStructure $Area | Where-Object { $_.Name -in $CSVColumnNames } # If the Visability is present, manually interpolate. $VisabilityParameter = $( if ($CSVColumnNames -contains 'Visibility') { Write-Output ',$Visibility'} else { Write-Output '' } ) # Param Block $ParamBlock = 'param($Area,$ContributionArea,{0}{1});' -f (($FormStructure.Name | ForEach-Object { "`${$_}" }) -join ','), $VisabilityParameter # Create the Scriptblock $ScriptBlock = $FormStructure | ForEach-Object -Begin { $ParamBlock } -Process { "Value '{0}' {1}" -f $_.Name, "`${$($_.Name)};" } # Return our Fixture Goodness return ([scriptblock]::Create($ScriptBlock)) } #BuildFileName: New-MVPActivity.ps1 function New-MVPActivity { [CmdletBinding(DefaultParameterSetName="Default")] param ( # Scriptblock of the Activity [Parameter(Mandatory,Position=1,ParameterSetName="Arguments")] [Parameter(Mandatory,Position=1,ParameterSetName="Default")] [ScriptBlock] $Fixture, # ArgumentList of the Activity [Parameter(Position=2,ParameterSetName="Arguments")] [HashTable] $ArgumentList ) begin { Write-Debug "[New-MVPActivity] BEGIN: Buildup of Activity" # # Activity Setup $invokeTearDown = $false $isPreParse = $true # Setup of Contributions and Areas $Script:MVPArea = $null $Script:ContributionAreas = [System.Collections.Generic.List[PSCustomObject]]::New() $Script:MVPHTMLFormStructure = $null try { # # Test the configuration is valid. $parameterCmdlets = Test-ActivityScriptBlock @PSBoundParameters # Test that the driver is active Test-SEDriver $isPreParse = $false # Test that the MVPActivity elements is present. Wait-ForMVPElement # Create new MVPActivity. Click the Button. Invoke-MVPActivity } catch { Write-Debug "[MVPActivity] Failed MVP Validation Error Below:" Write-Error $_ $invokeTearDown = $true } Write-Debug "[MVPActivity] BEGIN: Buildup Complete" } process { if ($invokeTearDown) { return } Write-Debug "[New-MVPActivity] PROCESS:" # Invoke the Fixture try { # If the Parameter is used instead of the cmdlet itself, invoke the cmdlet parsing in the parametervalue if ($parameterCmdlets.ParametrizedArea) { Area $ArgumentList.Area } if ($parameterCmdlets.ParametrizedContributionArea) { ContributionArea $ArgumentList.ContributionArea } if ($parameterCmdlets.ParametrizedVisibility) { Visibility $ArgumentList.Visibility } if ($ArgumentList) { # Invoke the Fixture Splatting the Args in as parameters. & $Fixture @ArgumentList } else { $null = $Fixture.Invoke() } # Save the MVP Activity Save-MVPActivity } catch { Write-Error $_ Write-Warning $LocalizedData.WarningEntryWasNotSaved Write-Debug "Error was triggered, initiate the teardown of the MVPActivity" $invokeTearDown = $true } } end { Write-Debug "[New-MVPActivity] END: Beginning Teardown" # # Activity Tear Down # try { if ($invokeTearDown -and (-not $isPreParse)) { # Close the MVP Activity Stop-MVPActivity } } finally { # TearDown of Contributions and Areas $Script:MVPArea = $null $Script:ContributionAreas = [System.Collections.Generic.List[PSCustomObject]]::New() $Script:MVPHTMLFormStructure = $null } Write-Debug "[New-MVPActivity] END: Completed" # # Wait for the Window to be closed # Wait-ForActivityWindow # # Return to the Caller # return $null } } #BuildFileName: Save-MVPActivity.ps1 function Save-MVPActivity { [cmdletbinding()] param() # Test if the Driver is active. If not throw a terminating error. Test-SEDriver # Validate that HTML Form Strucuture Prior to Submission # Ensure that all required items are set. $missingRequiredEntries = $Script:MVPHTMLFormStructure.Where{($_.isSet -eq $false) -and ($_.isRequired -eq $true)} if ($missingRequiredEntries) { Throw ($LocalizedData.ErrorMissingRequiredEntries -f (-join $missingRequiredEntries.Name)) # Fail the Submission and Tidy Up } # # Search for Field Validation Errors $fieldValidationErrors = Find-SeElement -Target $Global:MVPDriver -By ClassName -Selection $LocalizedData.ElementFieldValidationError if ($fieldValidationErrors) { Throw ($LocalizedData.ErrorFieldValidationError -f $fieldValidationErrors.Text) } # # Save the Activity $GetDriverParams = @{ Driver = $Global:MVPDriver Id = $LocalizedData.ElementButtonSubmitActivity } try { # Click the Save Button $SaveButton = Find-SeElement -Driver $Global:MVPDriver -Id $LocalizedData.ElementButtonSubmitActivity Invoke-SeClick -Element $SaveButton # Look for error 500's. The URL will approx redirect to: # https://mvp.microsoft.com/Error/500?aspxerrorpath=/ # There are MSFT Issues. Stop if ($Global:MVPDriver.Url -match $LocalizedData.MVPPortal500Error) { Throw $LocalizedData.Error500 } } catch { Throw ($LocalizedData.ErrorSavingMVPActivity -f $_) } } #BuildFileName: Select-DropDown.ps1 function Select-DropDown { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $elementId, [Parameter(Mandatory)] [string] $selectedValue ) try { $ActivityType = Find-SeElement -Driver $Global:MVPDriver -Id $elementId $SelectElement = New-Object -TypeName OpenQA.Selenium.Support.UI.SelectElement $ActivityType $SelectElement.SelectByValue($selectedValue) } catch { $PSCmdlet.ThrowTerminatingError($LocalizedData.ErrorSelectingDropDown -f $elementId, $selectedValue, $_) } } #BuildFileName: Stop-MVPActivity.ps1 function Stop-MVPActivity { [CmdletBinding()] param ( ) # Test if the Driver is active. If not throw a terminating error. Test-SEDriver try { $CancelButton = Find-SeElement -Driver $Global:MVPDriver -Id $LocalizedData.ElementButtonCancelActivity Invoke-SeClick -Element $CancelButton } catch { Throw ($LocalizedData.ErrorStopMVPActivity -f $_) } } #BuildFileName: Test-ActivityScriptBlock.ps1 function Test-ActivityScriptBlock { [CmdletBinding()] param ( # Scriptblock to invoke [Parameter(Mandatory)] [ScriptBlock] $Fixture, [Parameter()] [HashTable] $ArgumentList ) Write-Debug "[Test-ActivityScriptBlock] Validating Scriptblock:" # # Return an Object $resultObject = [PSCustomObject]@{ ParametrizedArea = $false ParametrizedContributionArea = $false ParametrizedVisibility = $false } # # Return a list of Commands $CommandAst = $Fixture.ast.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true) $AreaValue = $null $ContributionAreaValues = $null # Requirement 1: # Ensure that there are no additional MVPActivity scriptblocks within it. if ($CommandAst -match $LocalizedData.TestActivityRegexMVPActivity) { Throw $LocalizedData.ErrorNestedMVPActivity } # Requirement 2: # Ensure that a Area is present and it's a single instance. $areaInstance = $CommandAst -match $LocalizedData.TestActivityRegexMVPArea if (($areaInstance.count -eq 0) -and ($null -eq $ArgumentList.Area)) { # No Instances were found Throw $LocalizedData.ErrorMissingMVPActivityArea } elseif (($areaInstance.count -eq 0) -and ($null -ne $ArgumentList.Area)) { $resultObject.ParametrizedArea = $true $AreaValue = $ArgumentList.Area } elseif ($areaInstance.count -ne 1 ) { # Multiple Statements of Area was defined Throw $LocalizedData.ErrorMissingMVPActivityAreaMultiple } elseif (($areaInstance.count -ne 0) -and ($null -eq $ArgumentList.Area)) { $AreaValue = Get-ASTInstanceValues -InstanceValues $areaInstance -ParameterName 'Name' } # Requirement 3: # Ensure that the ContributionArea is present and that there is a maximum of two contributions. $ContributionAreaInstance = $CommandAst -match $LocalizedData.TestActivityRegexMVPContributionArea if ((($ContributionAreaInstance).Count -eq 0) -and ($null -eq $ArgumentList.ContributionArea)) { Throw $LocalizedData.ErrorMissingMVPActivityContributionArea } elseif (($ContributionAreaInstance.count -eq 0) -and ($null -ne $ArgumentList.ContributionArea)) { $resultObject.ParametrizedContributionArea = $true $ContributionAreaValues = $ArgumentList.ContributionArea } elseif (($ContributionAreaInstance).Count -gt 3) { Throw ($LocalizedData.ErrorExceedMVPActivityContributionArea -f $ContributionAreaInstance.Count) } elseif (($ContributionAreaInstance.count -ne 0) -and ($null -eq $ArgumentList.ContributionArea)) { $ContributionAreaValues = Get-ASTInstanceValues -InstanceValues $ContributionAreaInstance -ParameterName 'SelectedValue' } # Requirement 4: # Ensure that the Value Cmdlet is present. [Array]$valueInstances = $CommandAst -match $LocalizedData.TestActivityRegexMVPValue if ($valueInstances.Count -eq 0) { Throw $LocalizedData.ErrorMissingMVPActivityValue } # Requirement 5: # Visibility dosen't need to be included into the statement, however ensure that there are only one of them. [Array]$visibilityInstances = $CommandAst -match $LocalizedData.TestActivityRegexMVPVisibility if ($visibilityInstances.Count -gt 1) { Throw $LocalizedData.ErrorMultipleVisibilityStatements } # If Visability is included as a parameter, process it. elseif (($visibilityInstances.count -eq 0) -and ($null -ne $ArgumentList.Visibility)) { $resultObject.ParametrizedVisibility = $true } # Requirement 6: # Ensure that all Values added to the fixture are (required) AND are correct $HTMLFormStructure = Get-HTMLFormStructure -Name $AreaValue $ASTInstanceValues = Get-ASTInstanceValues -InstanceValues $valueInstances -ParameterName 'Name' $testMandatoryValues = $HTMLFormStructure | Where-Object { ( ($_.isRequired) -and ( ($_.Name -notin $ASTInstanceValues) -and ($_.Element -notin $ASTInstanceValues) ) )} $testMisnamedValues = $ASTInstanceValues | Where-Object { $_ -notin $HTMLFormStructure.Name -and $_ -notin $HTMLFormStructure.Element } if ($testMandatoryValues -or $testMisnamedValues) { Throw ($LocalizedData.ErrorParseValueCheck -f $AreaValue) } # Requirement 5: # Ensure that all ContributionAreas are added to the fixture are (required) AND are correct if ($ContributionAreaValues | Where-Object { $_ -notin $Global:HTMLContributionAreas}) { Throw ($LocalizedData.ErrorParseContributionAreaCheck -f $AreaValue) } # # Finished Write-Output $resultObject Write-Debug "[Test-ActivityScriptBlock] All Tests Passed:" } #BuildFileName: Test-CallStack.ps1 function Test-CallStack { [CmdletBinding()] param( # Parameter help description [Parameter(Mandatory)] [String] $Name ) if ((Get-PSCallStack).Command -notcontains "MVPActivity") { Throw ($LocalizedData.ErrorMissingMVPActivityInCallStack -f $Name) } } #BuildFileName: Test-CSVSchema.ps1 Function Test-CSVSchema { [cmdletbinding()] param( [Parameter(Mandatory)] [String] $LiteralPath ) $RequiredTopLevelColumns = @('Area','ContributionArea') # Test-Path if (-not(Test-Path @PSBoundParameters -ErrorAction SilentlyContinue)) { Throw ($LocalizedData.ErrorTestCSVSchemaMissingCSVFile -f $LiteralPath) } try { # Import the CSV File $CSV = Import-Csv @PSBoundParameters } catch { Throw ($LocalizedData.ErrorTestCSVSchemaImportCSVFile -f $_) } # Get the CSV Column Names $CSVColumnNames = ($CSV | Get-Member).Name # Validate if the Area/ ContributionArea Columns are present. $MissingTopLevelColumns = $RequiredTopLevelColumns | Where-Object { $CSVColumnNames -notcontains $_ } # If items were matched then throw a terminating error. if ($MissingTopLevelColumns.Count -ne 0) { Throw ($LocalizedData.ErrorTestCSVSchemaMissingColumns -f ($MissingTopLevelColumns -join ' , ')) } # Ensure that the Areas are all the same. Group by the Property Area $GroupedItems = $CSV | Group-Object -Property Area # Retrive the Area and Contribution Areas if ($GroupedItems.Length -ne 1) { Throw ($LocalizedData.ErrorTestCSVSchemaDifferentAreaColumn -f $GroupedItems.Name -join ' , ') } $Area = $GroupedItems | Select-Object -First 1 -ExpandProperty Name # Perform a Lookup to workout what columns we need $FormStructure = Get-HTMLFormStructure $Area | Where-Object {$_.isRequired} # Validate that the required Name or Element is Present within the CSV File $MissingCSVColumns = $FormStructure | Where-Object { $_.Name -notin $CSVColumnNames } if ($MissingCSVColumns.Count -ne 0) { Throw ($LocalizedData.ErrorTestCSVSchemaMissingColumns -f $MissingCSVColumns.Name -join ' , ') } # Validate no duplicate Contribution Area's are present ForEach($Row in $CSV) { if ((-not([String]::IsNullOrEmpty($Row.SecondContributionArea))) -or (-not($Row.ThirdContributionArea))) { $arr = $Row.ContributionArea, $Row.SecondContributionArea, $Row.ThirdContributionArea | Group-Object | Where-Object {$_.Count -ne 1} if ($arr.Count -ne 0) { Throw ($LocalizedData.ErrorTestCSVSchemaDuplicateContributionArea) } } } } #BuildFileName: Test-MVPDriverisMicrosoftLogin.ps1 function Test-MVPDriverisMicrosoftLogin { [CmdletBinding()] param ( [Parameter()] [Switch] $waitUntilLoaded, [Parameter()] [Switch] $isCompleted ) # The parameters provide pester guidance on how to mock this function Write-Output ($Global:MVPDriver.Url -match $LocalizedData.ConnectToMVPPortalRegexURLMatch) } #BuildFileName: Test-SEActivityTypes.ps1 function Test-SEActivityTypes { $result = $false # If there Driver Variable is $null. Throw a Terminating Error if ($null -ne $Global:SEActivityTypes) { $result = $true } Write-Output $result } #BuildFileName: Test-SEContributionAreas.ps1 function Test-SEContributionAreas { [cmdletbinding()] param() $result = $false # If there Driver Variable is $null. Throw a Terminating Error if ($null -ne $Global:SEContributionAreas) { $result = $true } Write-Output $result } #BuildFileName: Test-SEDriver.ps1 function Test-SEDriver { [CmdletBinding()] param() # If there Driver Variable is $null. Throw a Terminating Error if ($null -eq $Global:MVPDriver) { Throw ($LocalizedData.ErrorMissingDriver) } elseif ($null -eq $Global:MVPDriver.URL) { Throw ($LocalizedData.ErrorMissingDriver) } } #BuildFileName: Try-TentitiveCommand.ps1 function ttry { [CmdletBinding()] param ( [Parameter(Mandatory, Position = 0)] [Scriptblock] $Try, [Parameter(Mandatory, Position = 1)] [Scriptblock] $Catch, [Parameter(Position = 3)] [Int] $RetryLimit = 3 ) $Count = 0 Do { Try { $Result = $Try.Invoke() break } Catch { $Result = $Catch.Invoke() $Count++ } } Until ($Count -eq $RetryLimit) if ($Count -eq $RetryLimit) { Write-Error $LocalizedData.ErrorTryTentitiveCommand } Write-Debug "[Try-TentativeCommand] Returning:" return $Result } #Yup it's gotta be flipped, because I need to mock ttry and I can't mock and alias. Set-Alias -Name "Try-TentativeCommand" -Value "ttry" #BuildFileName: Wait-ForActivityWindow.ps1 function Wait-ForActivityWindow { [CmdletBinding()] param ( ) # Test the Selenium Driver Test-SEDriver # Start Verbose Write-Verbose "[Wait-ForActivityWindow] Started:" # Wait for the MVPActivity Window to be closed. try { $DialogElement = Find-SeElement -Driver ($Global:MVPDriver) -ClassName 'mvp-dialog' } Catch { Throw $_ } While (($null -ne $DialogElement) -and ($DialogElement.Displayed)) { Write-Verbose "[Wait-ForActivityWindow] Waiting for 'Add a new activity' to close:" Start-Sleep -Seconds 1 } # Start Verbose Write-Verbose "[Wait-ForActivityWindow] Finished:" } #BuildFileName: Wait-ForJavascript.ps1 Function Wait-ForJavascript { [CmdletBinding()] param ( [Parameter(Mandatory)] [String] $ElementTextToSearch ) Test-SEDriver # # Sometimes the Javascript dosen't load and add elements # to the HTML page. This checks to see if the element text is present # and if so, then continue. $retry = 0 Do { if (($Global:MVPDriver).PageSource -like ("*{0}*" -f $ElementTextToSearch)) { break } Start-Sleep -Seconds 1 $retry++ } Until ($retry -eq 3) if ($retry -eq 3) { Throw $LocalizedData.ErrorJavaScriptTimeout } } #BuildFileName: Wait-ForMVPElement.ps1 function Wait-ForMVPElement { [CmdletBinding()] param ( ) Write-Verbose "[Wait-ForMVPElement] Started:" ttry { $ActivityButton = Find-SeElement -Driver $Global:MVPDriver -Id $LocalizedData.ElementButtonNewActivity if ($null -eq $ActivityButton) { Enter-SeUrl 'https://mvp.microsoft.com/en-us/MyProfile/EditActivity' -Driver $Global:MVPDriver Throw "Missing" } } -catch { # Look for error 500's. The URL will approx redirect to: # https://mvp.microsoft.com/Error/500?aspxerrorpath=/ # There are MSFT Issues. Stop if ($Global:MVPDriver.Url -match $LocalizedData.MVPPortal500Error) { Throw $LocalizedData.Error500 } Write-Debug ("Waiting for New Activity Button. {0}" -f $LocalizedData.ElementButtonNewActivity) Write-Warning "Waiting for New Activity Button." } -RetryLimit 10 Write-Verbose "[Wait-ForMVPElement] Completed:" } #BuildFileName: Area.ps1 function Area { <# .Description Used inside of the MVPActivity block, the ‘Area’ Describes the Contribution Area. The Current List of Areas are: + Article + Blog/WebSite Post + Book (Author) + Book (Co-Author) + Conference (Staffing) + Docs.Microsoft.com Contribution + Forum Moderator + Forum Participation (3rd Party forums) + Forum Participation (Microsoft Forums) + Mentorship + Microsoft Open Source Projects + Non-Microsoft Open Source Projects + Organizer (User Group/Meetup/Local Events) + Organizer of Conference + Other + Product Group Feedback + Sample Code/Projects/Tools + Site Owner + Speaking (Conference) + Speaking (User Group/Meetup/Local events) + Technical Social Media (Twitter, Facebook, LinkedIn...) + Translation Review, Feedback and Editing + Video/Webcast/Podcast + Workshop/Volunteer/Proctor .PARAMETER SelectedValue The MVP Contribution Area .EXAMPLE Area 'Article' .SYNOPSIS 'Area' defines the MVP Contribution Area within the Portal. Area can only be used within MVPActivity. #> [CmdletBinding()] param( # Selected Value [Parameter(Mandatory=$true)] [String] $SelectedValue ) begin { # Test if the Driver is active. If not throw a terminating error. Test-SEDriver # Test the Callstack. Test-CallStack $PSCmdlet.MyInvocation.MyCommand.Name } Process { # Validate the $SelectedValue Attributes [Array]$matchedActivityType = Get-ActivityTypes | Where-Object { $_.Name -eq $SelectedValue } if ($matchedActivityType.Count -eq 0) { Throw ($LocalizedData.ErrorMissingSelectedValue -f $SelectedValue) } if ($matchedActivityType.Count -ne 1) { Throw ($LocalizedData.ErrorTooManySelectedValue -f $SelectedValue, $matchedActivityType.count) } # If the Activity Type matches, we need to do a lookup to get the HTML Form structure $HTMLFormStructure = Get-HTMLFormStructure $SelectedValue # Select the Element $output = ttry { Select-DropDown -elementId $LocalizedData.ElementIdActivityType -selectedValue $matchedActivityType.Value # We are using Views of Answers to dertmine if the Javascript has ran $HTMLFormStructure | ForEach-Object { Wait-ForJavascript -ElementText $_.Name } # Update the Area $Script:MVPArea = $SelectedValue $Script:MVPHTMLFormStructure = $HTMLFormStructure Write-Output $true } -Catch { # If the Javascript Fails to Populate the Sub entries within the form # it will retrigger by select the "Article" Start-Sleep -Seconds 1 Select-DropDown -elementId $LocalizedData.ElementIdActivityType -selectedValue $LocalizedData.ElementValueArticle } -RetryLimit 10 # If it failed to select the Area, we need to fail the cmdlet if ($output -ne $true) { # Throw a terminating error Throw $LocalizedData.ErrorAreaFailure } } } #BuildFileName: ConnectTo-MVPPortal.ps1 function ConnectTo-MVPPortal { <# .Description Connects to the MVP Portal and waits for the login to complete. .PARAMETER URLPath A custom URL provided in the MVP email. Otherwise, if excluded, it will prompt the user for a URL. .PARAMETER DriverType You can specify different browser types. The default is Firefox. Options are: Firefox, Chrome, Edge, OldEdge .EXAMPLE Connect to Portal: ConnectTo-MVPPortal -URLPath "https://Your-URL-Path-Goes-Here.com" .EXAMPLE Connect to Portal: ConnectTo-MVPPortal -URLPath "https://Your-URL-Path-Goes-Here.com" -DriverType ('Firefox','Chrome' or 'Edge') .SYNOPSIS Connects to the MVP Portal and waits for the login to complete. #> [CmdletBinding()] param ( [Parameter()] [String] $URLPath=(Read-Host "Enter in URL Path"), [ValidateSet("Firefox","Chrome","Edge", "OldEdge")] [String] $DriverType = 'Firefox' ) try { switch ($DriverType) { 'Firefox' { $Global:MVPDriver = Start-SeFirefox -StartURL $URLPath } 'Chrome' { $Global:MVPDriver = Start-SeChrome -StartURL $URLPath } 'Edge' { $Global:MVPDriver = Start-SeNewEdge -StartURL $URLPath } 'OldEdge' { $Global:MVPDriver = Start-SeEdge -StartURL $URLPath } } } catch { $Global:MVPDriver = $null Throw ($LocalizedData.ErrorConnectToMVPPortal -f $_) } # Wait until the Login Screen Loads do { Test-SEDriver Write-Verbose "Waiting for the URL to redirect to the Microsoft Login" Start-Sleep -Seconds 1 } Until (Test-MVPDriverisMicrosoftLogin -waitUntilLoaded) # Then Pause Execution until login process has completed while (Test-MVPDriverisMicrosoftLogin -isCompleted) { Test-SEDriver Write-Verbose "Waiting for Login to complete:" Start-Sleep -Seconds 1 } Write-Verbose "Success! Logged in." } #BuildFileName: ContributionArea.ps1 Function ContributionArea { <# .Description The ContributionArea defines the Contribution Areas within a contribution. ContributionArea can be used as a String Array (ContributionArea 'Area1','Area2','Area3'), or by specifying multiple ContributionArea statements. Similarly to Area, if ContributionArea is included within the Param() block, it's not required to be defined and will be automatically invoked. The current list of 'Contribution Areas' are: + Chef/Puppet in Datacenter + Container Management + Datacenter Management + Group Policy + High Availability + Hyper-V + Linux in System Center/Operations Management Suite + Linux on Hyper-V + Networking + PowerShell + Storage + Windows Server for Small & Medium Business + Azure Bot Service + Azure Cognitive Search + Azure Cognitive Services + Azure Machine Learning + Dynamics 365 + Power Apps + Power Automate + Power Pages + Power Virtual Agents + Azure Arc Enabled Data Services + Azure Cosmos DB + Azure Data Catalog + Azure Data Explorer + Azure Data Lake + Azure Database for MySQL + Azure Database for PostgreSQL + Azure Databricks + Azure HDInsight and Hadoop & Spark on Azure + Azure Search + Azure SQL (Database, Pools, Serverless, Hyperscale, Managed Instance, Virtual Machines) + Azure SQL Edge + Azure Stream Analytics + Azure Synapse Analytics + Big Data Clusters + Cortana Intelligence Suite + Data Warehousing (Azure SQL Data Warehouse, Fast Track & APS) + Information Management (ADF, SSIS, & Data Sync) + Microsoft Purview + Power BI + SQL Server (on Windows, Linux, Containers) + SQL Server Machine Learning Services (R, Python) + SQL Server Reporting Services & Analysis Services + Tools & Connectivity + .NET + Accessibility + ASP.NET/IIS + C++ + Developer Security + Front End Web Dev + Github & Azure DevOps + Java + Javascript/Typescript + Node.js + Python + Quantum + Unity + Visual Studio Code + Visual Studio Extensibility + Xamarin + Information Protection + Microsoft Intune + Previous Expertise: Directory Services + Remote Desktop Services + Azure Edge Devices + Azure IoT Services & Development + Access + Excel + Exchange + Microsoft Stream + Microsoft Teams + Microsoft Viva + Office 365 + OneDrive + OneNote + Outlook + PowerPoint + Project + SharePoint + Skype for Business + Visio + Word + Yammer + Microsoft Graph + Microsoft Teams Development + Outlook Development + SharePoint Development + W/X/P Development + ARM Templates (Infra as Code) + Azure API Management + Azure App Service + Azure Arc Enabled Infrastructure + Azure Backup & Disaster Recovery + Azure Blockchain + Azure Core Compute (VMSS, Confidential Computing, Platform Deployment) + Azure Cost Management + Azure Functions + Azure Hybrid + Azure Kubernetes, Container Instances, Docker + Azure Lighthouse + Azure Logic Apps + Azure Migrate + Azure Monitor + Azure Networking + Azure Policy & Governance + Azure SDK (Software Development Kit) and CLIs ( Az CLI, PowerShell, Terraform, Ansible) + Azure Service Fabric + Azure Storage + Distributed App Runtime (Dapr), Open App Model (OAM), Open Service Mesh (OSM) + Previous Expertise: Microsoft Azure + Service Bus, Event Hubs, Event Grid, Relay + D365 Mixed Reality + MR Design + MR Development + Cloud Security + Identity & Access + SIEM & XDR + Surface for IT + Windows 365 + Windows for IT + Windows Design + Windows Development .PARAMETER SelectedValue The Contribution Areas that you wish to add. .EXAMPLE Single Use: ContributionArea 'Area1' .EXAMPLE ContributionArea can be stacked like so: ContributionArea 'Area1' ContributionArea 'Area2' ContributionArea 'Area3' .EXAMPLE ContributionArea can placed in an array: ContributionArea 'Area1','Area2','Area3' .SYNOPSIS The ContributionArea is a command that defines the Contribution Areas within the MVP Portal. #> [cmdletbinding()] param( # Parameter help description [Parameter(Mandatory)] [String[]] $SelectedValue ) begin { # Test if the Driver is active. If not throw a terminating error. Test-SEDriver # Test the Callstack. Test-CallStack $PSCmdlet.MyInvocation.MyCommand.Name } process { # Multiple Contribution Areas can be parsed in. ForEach ($Param in $SelectedValue) { # Fetch the Contribution Areas (sometimes the browser is too quick) $HTMLContributionAreas = ttry { Get-ContributionAreas } -Catch { Start-Sleep -Milliseconds 500 } -RetryLimit 5 # Validate the $SelectedValue Parameters [Array]$matchedActivityType = $HTMLContributionAreas | Where-Object { $_.Name -eq $Param } if ($matchedActivityType.Count -eq 0) { Throw ($LocalizedData.ErrorMissingSelectedValue -f $Param) } if ($matchedActivityType.Count -ne 1) { Throw ($LocalizedData.ErrorTooManySelectedValue -f $Param, $matchedActivityType.count) } # Generate the ElementId $elementId = $( if ($Script:ContributionAreas.Count -eq 0) { $LocalizedData.ElementIdContributionArea } else { "{0}{1}" -f $LocalizedData.ElementIdContributionArea, ($Script:ContributionAreas.Count + 1) } ) # Add to the Contribution Areas $Script:ContributionAreas.Add( [PSCustomObject]@{ elementId = $matchedActivityType.Name selectedValue = $matchedActivityType.Value } ) # # If the $Script:ContributionAreas count is gt then 3, then select the 'add more' link if ($Script:ContributionAreas.count -ge 3) { $AddMoreButton = Find-SeElement -Driver $Global:MVPDriver -By ClassName -Selection 'add' Invoke-SeClick -Element $AddMoreButton } # # You will need to inspect the elements on the HTML to add additional drop down settings. # Make sure you set the id as the value ttry { Select-DropDown -elementId $elementId -selectedValue $matchedActivityType.Value # We are using Views of answers to dertmine if the Javascript has ran Wait-ForJavascript -ElementText 'Views of answers' } -Catch { # If the Javascript Fails to Populate the Sub entries within the form # it will retrigger by select the "Chef/Puppet in Datacenter" Start-Sleep -Seconds 1 Select-DropDown -elementId $Script:ContributionAreas[-1].elementId -selectedValue $LocalizedData.ElementValueChefPuppetInDataCenter } -RetryLimit 10 } } } #BuildFileName: Get-AreaNamedValues.ps1 function Get-AreaNamedValues { <# .Description This command is used to retrieve the list of fields that are required for an MVP submission. Since different MVP Area's have other fields, you can use Get-AreaNamedValues 'AreaName' to identify the fields. In the example below, we will use Get-AreaNamedValues 'Article' to get the Value names: Get-AreaNamedValues Article Name Mandatory ---- --------- Number of Articles True Title True Date True Number of Views False URL False Description False Once the list is returned, we can construct the data structure using the output: MVPActivity "Test" { # Let's set the Area and the Contribution Area Area 'Article' ContributionArea 'PowerShell' # We can set the mandatory parameters Value "Number of Articles" 1 Value Title "Test Entry" Value Date "30/11/2020" } .PARAMETER AreaName The MVP Contribution Area name to lookup. .EXAMPLE In the example below, we will use Get-AreaNamedValues 'Article' to get the Value names: Get-AreaNamedValues Article Name Mandatory ---- --------- Number of Articles True Title True Date True Number of Views False URL False Description False .SYNOPSIS This command is used to retrive the list of fields that are required for a MVP submission. #> [CmdletBinding()] param ( [Parameter()] [String] $AreaName ) # Call the HTML FormStrucutre and Parse in the Article Name Get-HTMLFormStructure $AreaName | Select-Object Name, @{Name='Mandatory';Exp={$_.isRequired}} | Sort-Object -Property Mandatory -Descending } #BuildFileName: MVPActivity.ps1 function MVPActivity { <# .Description MVPActivity is the top-level definition command, which groups the MVP contribution types into their respective areas. This command is responsible for buildup (creation) and (tear-down) of the contribution. There are two types of input with this command: 1. CSV Import and, 2. The Domain Specific Language. The CSV Import is a 'simplified' input parameter, however *each* CSV import is limited to a seperate 'Area'. If you choose to use the Domain Specific Language type, commands: 'Area', 'ContributionArea' and 'Value' are mandatory. MVPActivity also performs input validation ensuring that input meets the requirements of the HTML form. .PARAMETER Name Name is the name of the MVPActivity. This provides a visual grouping of data that is being added to the MVP Portal. .PARAMETER CSVPath Import the CSV File and Parse it. Please Note: *The headers 'Area' & 'ContributionArea' are mandatory.* Adding Secondary and Tertiary 'ContributionAreas' is done by using: 'SecondContributionArea' and 'ThirdContributionArea' (See Example Below) MVPActivity -CSVPath 'Path to CSV File' Example CSV File: Date,Title,URL,Description,Number of Articles,Number of Views,Area,ContributionArea,SecondContributionArea,ThirdContributionArea 28/11/2020,TEST,https://www.google.com,TEST,1,1,Article,PowerShell,Networking,Storage 28/11/2020,TEST,https://www.google.com,TEST,1,1,Article,PowerShell,Networking,Storage .PARAMETER ArgumentList MVPActivity supports arguments being parsed into it using the -ArgumentList parameter, similarly to the -TestCases parameter within Pester. Inputs are defined as a [HashTable] or a [HashTable[]] with the name of the parameter being the [HashTable] key and it's value being the item. To use the parameters that have been parsed into the fixture, use the param() block at the beginning of the fixture with the ParameterName. In the example below we declare the parameters and parse them into MVPActivity: # Declare the parameters: $param = @{ Parameter1 = 'Test' } # Parse the Arguments by using the -ArgumentList parameter: MVPActivity "Test Entry" -ArgumentList $param { # To get the value from $Parameter1, add a param() within the fixture/scriptblock. param($Parameter1) # We can reference the value by referencing the variable name '$Parameter1' Area $Parameter1 'Value' } The -ArgumentList parameter also supports an array of hashtables ([HashTable[]]) with common values, providing a way to parse multiple parameters into the same fixture. For Example: # Declare the parameters as an array: $param = @( @{ Parameter1 = 'One Value' } @{ Parameter1 = 'Another Value' } ) MVPActivity "Test Entry" -ArgumentList $param { param($Parameter1) Area $Parameter1 'Value' } This will process 'Parameter1' as a seperate MVP Contribution. .PARAMETER Fixture The Fixture/Scriptblock contains the DSL PowerShell code that adds the MVP entry to the portal. The Fixture is a regular Powershell scriptblock, however Area, ContributionArea and Value are mandatory commands. *Please ensure that 'Area', 'ContributionArea' are prefixed before 'Value'*. .EXAMPLE Import the CSV File and Parse it: MVPActivity -CSVPath 'Path to CSV File' .EXAMPLE Standard Usage: MVPActivity "Reddit Contribution" { Area 'Article' # ContributionArea can accept an Array ContributionArea 'PowerShell','Yammer','Word' Value 'Date' $date Value 'Title' 'TEST' Value 'URL' 'https:\\test.com' Value 'Description' 'THIS IS A TEST' Value 'Number of Posts' '1' # Or you can use the HTML DivId Value 'Number of Subscribers' '1' Value 'Annual Unique Visitors' '1' } .EXAMPLE In this example below, we will parse a hashtable of arguments into the fixture: $params = @{ Area = "Blog/Website Post" ContributionArea = "PowerShell","Yammer","Word" date = '26/10/2020' } # Define Activity MVPActivity "Name of Activity" -ArgumentList $params { # If the Parameters -Area or -ContributionArea are defined # in the param block, the cmdlet is not required (Area or ContributionArea). param($Area, $ContributionArea, $date) # You can use the String Name Value 'Date' $date Value 'Title' 'TEST' Value 'URL' 'https:\\test.com' Value 'Description' 'THIS IS A TEST' Value 'Number of Posts' '1' # Or you can use the HTML Div Id Value 'Number of Subscribers' '1' Value 'Annual Unique Visitors' '1' # Still can execute PowerShell within the script block Start-Sleep -Seconds 3 } .EXAMPLE For those who love complexity, the fixture supports an array of HashTables to be parsed into the fixture. $params = @( @{ Area = "Blog/Website Post" ContributionArea = "PowerShell","Yammer","Word" date = '26/10/2020' } @{ Area = "Blog/Website Post" ContributionArea = "PowerShell","Yammer","Word" date = '15/10/2020' } ) # Define Activity MVPActivity "Name of Activity" -ArgumentList $params { # If the Parameters -Area or -ContributionArea are defined in the param block, # the cmdlet is not required (Area or ContributionArea). param($Area, $ContributionArea, $date) # You can use the String Name Value 'Date' $date Value 'Title' 'TEST' Value 'URL' 'https:\\test.com' Value 'Description' 'THIS IS A TEST' Value 'Number of Posts' '1' # Or you can use the HTML Div Id Value 'Number of Subscribers' '1' Value 'Annual Unique Visitors' '1' # Still can execute PowerShell within the script block Start-Sleep -Seconds 3 } .SYNOPSIS MVPActivity is the top-level definition command, which groups the MVP contribution types into their respective areas. #> [CmdletBinding(DefaultParameterSetName="Default")] param ( # Scriptblock of the Name of the Contribution [Parameter(Mandatory,Position=1,ParameterSetName="Default")] [Parameter(Mandatory,Position=1,ParameterSetName="Arguments")] [Parameter(Position=1,ParameterSetName="CSVFile")] [String] $Name, # Scriptblock of the Activity [Parameter(Mandatory,Position=2,ParameterSetName="Arguments")] [Parameter(Mandatory,Position=2,ParameterSetName="Default")] [ScriptBlock] $Fixture, # ArgumentList of the Activity [Parameter(Position=3,ParameterSetName="Arguments")] [HashTable[]] $ArgumentList, # CSV File [Parameter(Position=2,ParameterSetName="CSVFile")] [String] $CSVPath ) # Construct the Parameters to Invoke $params = @{} switch ($PSCmdlet.ParameterSetName) { "Arguments" { $params.Fixture = $Fixture } "CSVFile" { # Test the CSV Schema to ensure that all the columns are present Test-CSVSchema $CSVPath # Create the Fixture $params.Fixture = New-CSVFixture $CSVPath # Create the Arguements for the Fixture $ArgumentList = New-CSVArguments $CSVPath } default { # If the Default Execution is run, execute the Fixture and then return. $params.Fixture = $Fixture } } # Write-Host if ($Name) { Write-Host ('Executing MVPActivity: "{0}"' -f $Name) -ForegroundColor Green } else { Write-Host ('Executing MVPActivity: (From CSV File) "{0}"' -f $CSVPath) -ForegroundColor Green } # If Arguments are Present, then iterate each Fixture if ($ArgumentList) { # Iterate Through Each of the Arguments and Create the MVPActivity foreach($Argument in $ArgumentList) { $params.ArgumentList = $Argument New-MVPActivity @params } } else { # Otherwise Execute the MVP Activity, since there is not additional fixtures. New-MVPActivity @params } } #BuildFileName: New-MVPFixture.ps1 function New-MVPFixture { <# .Description New-MVPFixture is a utility that generates MVPActivity ScriptBlocks. .PARAMETER AreaName The MVP Contribution Area name to lookup. .PARAMETER MVPActivityName The MVP Activity Name. .EXAMPLE Standard Execution: New-MVPFixture -AreaName 'Article' -MVPActivityName 'Test' .SYNOPSIS New-MVPFixture is a utility that generates MVPActivity ScriptBlocks. #> [CmdletBinding()] param ( [String] $AreaName=(Read-Host "Enter in an AreaName"), [String] $MVPActivityName=(Read-Host "Enter in an MVPActivity Name") ) $FormStructure = Get-AreaNamedValues -AreaName $AreaName return (" +==================== START COPY ====================+ MVPActivity '$MVPActivityName' { # Param Block for arguments to be parsed in. param() # Define the Area Area '$AreaName' # Define the Contribution Area ContributionArea 'Contribution Area 1', 'Contribution Area 2', 'Contribution Area 3' # Define the Input Values: $( $FormStructure | ForEach-Object { "`n", " #Mandatory: $($_.Mandatory) `n", " Value '$($_.Name)' '' `n" } ) } +==================== STOP COPY ====================+ ") } #BuildFileName: Value.ps1 Function Value { <# The 'Value' command is used as input entries for the HTML form, using 'Name' (Being the HTML Div Element ID or Text Name) and 'Value' (Corresponding Value) syntax. Since different 'Area''s have various fields, you can use Get-AreaNamedValues 'AreaName' to locate those fields. Value is mandatory within MVPActivity and won't be automatically invoked when specified within the Param() bock. Value supports auto-formatting of inputted data to meet the requirements of the MVP Portal. Currently, the following Portal dependencies are auto-formatted: Date (Required to be US Date Format) (MM-DD-YYYY) URL (Required to meet URL format) (https://site.com) To view the list of Values for an MVP Contribution Area, use: Get-AreaNamedValues 'Name' (Get-AreaNamedValues 'Article') .PARAMETER Name The HTML Div Element ID or Text Name .PARAMETER Value The input value. .EXAMPLE In the example, 'Value' is being used to set the HTML values for the 'Article' MVP Contribution Area. MVPActivity "Test" { # Let's set the Area and the Contribution Area Area 'Article' # We can set the mandatory parameters Value "Number of Articles" 1 Value Title "Test Entry" Value Date "30/11/2020" } .SYNOPSIS The Value command is used to input the data into the HTML form, using the 'Name' (Being the HTML Div Element ID or Text Name) and 'Value' (Corresponding Value) syntax. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [String] $Name, [Parameter(Mandatory)] [String] $Value ) # Test if the Driver is active. If not throw a terminating error. Test-SEDriver # Test the Callstack. Test-CallStack $PSCmdlet.MyInvocation.MyCommand.Name # Get the HTML Element Data $htmlElementData = $Script:MVPHTMLFormStructure # Attempt to Locate the HTML Element that is needed to update. # If there is no value, then there is no data [array]$matched = $htmlElementData.Where{($_.Name -eq $Name) -or ($_.Element -eq $Name)} # If the Either The Element of the String name can't be found, # then Throw a terminating error. if ($matched.count -eq 0) { Throw ($LocalizedData.ErrorCannotFindHTMLElement -f $Name) } # If there is more then 1 result, data duplicate. Throw temrinating error. if ($matched.count -ne 1) { Throw ($LocalizedData.ErrorTooManyHTMLElements -f $Name, $matched.count) } $params = @{ Keys = $Value } # If there is a formatting property, then we invoke the scriptblock and format the data if ($matched.Format) { try { # Invoke the Scriptblock $params.Keys = $matched.Format.Invoke($Value) } catch { # Throw an Error if there was a problem. Throw ($LocalizedData.ErrorFormattingValue -f $Name, $Value, $_.ToString()) } } # Fetches the Element and Updates the Field $Element = Find-SeElement -Driver ($Global:MVPDriver) -Id $matched[0].Element if ($null -eq $Element) { return } Send-SeKeys $Element @params # Locate and Update the HTMLElement $matched[0].isSet = $true } #BuildFileName: Visibility.ps1 Function Visibility { <# The 'Visibility' Parameter sets the visibility of the Activity. This statement can be included within the fixture or executed within the Param Block, similar to 'Area' and 'Contribution Area'. Multiple statements aren't allowed within the Fixture. .PARAMETER Name The Visibility Name: Values can be: 'Everyone','MVP Community' & 'Microsoft'. .EXAMPLE In the example, 'Visibility' is set within the Fixture: MVPActivity "Test" { # Let's set the Area and the Contribution Area Area 'Article' Visibility 'Microsoft' # We can set the mandatory parameters Value "Number of Articles" 1 Value Title "Test Entry" Value Date "30/11/2020" } .EXAMPLE In the example, 'Visibility' is set within the param block: MVPActivity "Test" { param($Visibility) # Let's set the Area and the Contribution Area Area 'Article' # We can set the mandatory parameters Value "Number of Articles" 1 Value Title "Test Entry" Value Date "30/11/2020" } .SYNOPSIS Set's the Visibility of the entry. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [String] [ValidateSet('Everyone','MVP Community','Microsoft')] $Name ) # Test if the Driver is active. If not throw a terminating error. Test-SEDriver # Test the Callstack. Test-CallStack $PSCmdlet.MyInvocation.MyCommand.Name # Locate the Visability Element. $VisibilityElement = Find-SeElement -Driver ($Global:MVPDriver) -By XPath -Selection $LocalizedData.ElementVisibilityBoxXPath if ($null -eq $VisibilityElement) { return } # Match the Name of the Element. If the name is the same, skip! if ($Name -eq $VisibilityElement.Text) { return } # Select the Element and Select the Correct Item Invoke-SeClick -Element $VisibilityElement # Locate the ListItem Permission $PermissionsParams = @{ Driver = $Global:MVPDriver By = 'XPath' Selection = $LocalizedData.VisibilityListItem -f $Name } $PermissionElement = Find-SeElement @PermissionsParams # Select the DropDown Item Invoke-SeClick -Element $PermissionElement } |