├── LICENSE ├── PDP ├── InAppProductDescription.xml ├── InAppProductDescription.xsd └── ProductDescription.xml ├── StoreBroker.nuspec ├── AzureService ├── RESTProxyContent │ ├── bin │ │ └── StartupTasks │ │ │ ├── TimeZone │ │ │ ├── EnsureTimeZone.cmd │ │ │ └── Ensure-TimeZone.ps1 │ │ │ ├── RemoteDesktop │ │ │ ├── EnsureRemoteDesktopEnabled.cmd │ │ │ └── Ensure-RemoteDesktopEnabled.ps1 │ │ │ ├── LocalAdmin │ │ │ ├── EnsureLocalAdmin.cmd │ │ │ └── Ensure-LocalAdmin.ps1 │ │ │ ├── DomainJoined │ │ │ ├── EnsureDomainJoined.cmd │ │ │ └── Ensure-DomainJoined.ps1 │ │ │ ├── FixAuth │ │ │ └── FixAuthentication.cmd │ │ │ └── Helpers │ │ │ ├── GroupManagement.ps1 │ │ │ └── Encryption.ps1 │ └── diagnostics.wadcfgx ├── Profiles │ ├── StoreBrokerProxyStaging.azurePubxml │ └── StoreBrokerProxyProduction.azurePubxml ├── ServiceConfiguration.Local.cscfg ├── ServiceConfiguration.Cloud.cscfg ├── AzureService.ccproj └── ServiceDefinition.csdef ├── StoreBroker.sln ├── SECURITY.md ├── StoreBroker.pssproj ├── Tests └── PackageTool.Tests.ps1 ├── .gitignore ├── README.md ├── StoreBroker ├── StoreBroker.psd1 ├── IapConfigTemplate.json ├── NugetTools.ps1 └── AppConfigTemplate.json ├── Documentation ├── GOVERNANCE.md └── PDP.md ├── CONTRIBUTING.md └── Extensions └── ConvertFrom-ExistingIapSubmission.ps1 /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/StoreBroker/HEAD/LICENSE -------------------------------------------------------------------------------- /PDP/InAppProductDescription.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | <!-- [required] --> 9 | <!-- _locComment_text="{MaxLength=100} IAP Title"--> 10 | My Localized App Add-on IAP 11 | 12 | 13 | 14 | 15 | 16 | This add-on will provide you with 200 extra coins. 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /StoreBroker.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Microsoft.Windows.StoreBroker 5 | $version$ 6 | Microsoft 7 | microsoft,nugetstorebrokerteam 8 | https://aka.ms/StoreBroker_License 9 | https://aka.ms/StoreBroker 10 | http://go.microsoft.com/fwlink/?LinkID=288890 11 | true 12 | A PowerShell module that leverages the Windows Store Submission API to allow easy automation of application submissions to the Windows Store. 13 | © Microsoft Corporation. All rights reserved. 14 | microsoft windows store broker storebroker submission api powershell posh module 15 | en-US 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /AzureService/RESTProxyContent/bin/StartupTasks/TimeZone/EnsureTimeZone.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | :: Log the startup date and time. 4 | SET timehour=%time:~0,2% 5 | SET timestamp=%date:~-4,4%%date:~-10,2%%date:~-7,2%-%timehour: =0%%time:~3,2% 6 | SET startuptasklogsubdir=TimeZone 7 | SET startuptasklogdir=%PathToStartupLogs%\%startuptasklogsubdir%\ 8 | SET startuptasklog=%startuptasklogdir%\startup-%timestamp%.txt 9 | 10 | IF NOT EXIST %startuptasklogdir% MKDIR %startuptasklogdir% 11 | 12 | IF "%ComputeEmulatorRunning%" == "true" ( 13 | ECHO Detected running in an emulator. Skipping setting the time zone. 2>&1 14 | ) ELSE ( 15 | ECHO Starting up %0. >> "%startuptasklog%" 2>&1 16 | 17 | :: Call the file, redirecting all output to the StartupLog.txt log file. 18 | powershell -executionpolicy unrestricted ". %0\..\Ensure-TimeZone.ps1; Ensure-TimeZone -TimeZoneId '%TimeZone%' -Verbose" >> "%startuptasklog%" 2>&1 19 | ) 20 | 21 | :: Log the completion of file/ 22 | ECHO Returned to %0. >> "%startuptasklog%" 2>&1 23 | 24 | IF %ERRORLEVEL% EQU 0 ( 25 | :: No errors occurred. Exit normally. 26 | ECHO Done >> "%startuptasklog%" 27 | EXIT /B 0 28 | ) ELSE ( 29 | :: Log the error. 30 | ECHO An error occurred. The ERRORLEVEL = %ERRORLEVEL%. >> "%startuptasklog%" 2>&1 31 | ECHO Done >> "%startuptasklog%" 32 | EXIT /B %ERRORLEVEL% 33 | ) -------------------------------------------------------------------------------- /AzureService/RESTProxyContent/bin/StartupTasks/RemoteDesktop/EnsureRemoteDesktopEnabled.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | :: Log the startup date and time. 4 | SET timehour=%time:~0,2% 5 | SET timestamp=%date:~-4,4%%date:~-10,2%%date:~-7,2%-%timehour: =0%%time:~3,2% 6 | SET startuptasklogsubdir=RemoteDesktop 7 | SET startuptasklogdir=%PathToStartupLogs%\%startuptasklogsubdir%\ 8 | SET startuptasklog=%startuptasklogdir%\startup-%timestamp%.txt 9 | 10 | IF NOT EXIST %startuptasklogdir% MKDIR %startuptasklogdir% 11 | 12 | IF "%ComputeEmulatorRunning%" == "true" ( 13 | ECHO Detected running in an emulator. Skipping enabling remote desktop. 2>&1 14 | ) ELSE ( 15 | ECHO Starting up %0. >> "%startuptasklog%" 2>&1 16 | 17 | :: Call the file, redirecting all output to the StartupLog.txt log file. 18 | powershell -executionpolicy unrestricted ". %0\..\Ensure-RemoteDesktopEnabled.ps1; Ensure-RemoteDesktopEnabled -Verbose" >> "%startuptasklog%" 2>&1 19 | ) 20 | 21 | :: Log the completion of file/ 22 | ECHO Returned to %0. >> "%startuptasklog%" 2>&1 23 | 24 | IF %ERRORLEVEL% EQU 0 ( 25 | :: No errors occurred. Exit normally. 26 | ECHO Done >> "%startuptasklog%" 27 | EXIT /B 0 28 | ) ELSE ( 29 | :: Log the error. 30 | ECHO An error occurred. The ERRORLEVEL = %ERRORLEVEL%. >> "%startuptasklog%" 2>&1 31 | ECHO Done >> "%startuptasklog%" 32 | EXIT /B %ERRORLEVEL% 33 | ) -------------------------------------------------------------------------------- /AzureService/RESTProxyContent/bin/StartupTasks/LocalAdmin/EnsureLocalAdmin.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | :: Log the startup date and time. 4 | SET timehour=%time:~0,2% 5 | SET timestamp=%date:~-4,4%%date:~-10,2%%date:~-7,2%-%timehour: =0%%time:~3,2% 6 | SET startuptasklogsubdir=EnsureLocalAdmin 7 | SET startuptasklogdir=%PathToStartupLogs%\%startuptasklogsubdir%\ 8 | SET startuptasklog=%startuptasklogdir%\startup-%timestamp%.txt 9 | 10 | IF NOT EXIST %startuptasklogdir% MKDIR %startuptasklogdir% 11 | 12 | IF "%ComputeEmulatorRunning%" == "true" ( 13 | ECHO Detected running in an emulator. Skipping admin creation. 2>&1 14 | ) ELSE ( 15 | ECHO Starting up %0. >> "%startuptasklog%" 2>&1 16 | 17 | :: Call the file, redirecting all output to the StartupLog.txt log file. 18 | powershell -executionpolicy unrestricted ". %0\..\Ensure-LocalAdmin.ps1; Ensure-LocalAdmin -userName '%LOCAL_ADMIN_USERNAME%' -secret '%LOCAL_ADMIN_SECRET%' -certThumbprint '%LOCAL_ADMIN_CERT_THUMBPRINT%' -certStore '%LOCAL_ADMIN_CERT_STORE%' -Verbose" >> "%startuptasklog%" 2>&1 19 | ) 20 | 21 | :: Log the completion of file/ 22 | ECHO Returned to %0. >> "%startuptasklog%" 2>&1 23 | 24 | IF %ERRORLEVEL% EQU 0 ( 25 | :: No errors occurred. Exit normally. 26 | ECHO Done >> "%startuptasklog%" 27 | EXIT /B 0 28 | ) ELSE ( 29 | :: Log the error. 30 | ECHO An error occurred. The ERRORLEVEL = %ERRORLEVEL%. >> "%startuptasklog%" 2>&1 31 | ECHO Done >> "%startuptasklog%" 32 | EXIT /B %ERRORLEVEL% 33 | ) -------------------------------------------------------------------------------- /AzureService/RESTProxyContent/bin/StartupTasks/DomainJoined/EnsureDomainJoined.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | :: Log the startup date and time. 4 | SET timehour=%time:~0,2% 5 | SET timestamp=%date:~-4,4%%date:~-10,2%%date:~-7,2%-%timehour: =0%%time:~3,2% 6 | SET startuptasklogsubdir=EnsureDomainJoined 7 | SET startuptasklogdir=%PathToStartupLogs%\%startuptasklogsubdir%\ 8 | SET startuptasklog=%startuptasklogdir%\startup-%timestamp%.txt 9 | 10 | IF NOT EXIST %startuptasklogdir% MKDIR %startuptasklogdir% 11 | 12 | IF "%ComputeEmulatorRunning%" == "true" ( 13 | ECHO Detected running in an emulator. Skipping domain join. 2>&1 14 | ) ELSE ( 15 | ECHO Starting up %0. >> "%startuptasklog%" 2>&1 16 | 17 | :: Call the file, redirecting all output to the StartupLog.txt log file. 18 | powershell -executionpolicy unrestricted ". %0\..\Ensure-DomainJoined.ps1; Ensure-DomainJoined -Domain '%DOMAIN_NAME%' -UserName '%DOMAIN_USERNAME%' -Secret '%DOMAIN_SECRET%' -CertThumbprint '%DOMAIN_CERT_THUMBPRINT%' -CertStore '%DOMAIN_CERT_STORE%' -Verbose" >> "%startuptasklog%" 2>&1 19 | ) 20 | 21 | :: Log the completion of file/ 22 | ECHO Returned to %0. >> "%startuptasklog%" 2>&1 23 | 24 | IF %ERRORLEVEL% EQU 0 ( 25 | :: No errors occurred. Exit normally. 26 | ECHO Done >> "%startuptasklog%" 27 | EXIT /B 0 28 | ) ELSE ( 29 | :: Log the error. 30 | ECHO An error occurred. The ERRORLEVEL = %ERRORLEVEL%. >> "%startuptasklog%" 2>&1 31 | ECHO Done >> "%startuptasklog%" 32 | EXIT /B %ERRORLEVEL% 33 | ) -------------------------------------------------------------------------------- /AzureService/RESTProxyContent/bin/StartupTasks/FixAuth/FixAuthentication.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | :: Log the startup date and time. 4 | SET timehour=%time:~0,2% 5 | SET timestamp=%date:~-4,4%%date:~-10,2%%date:~-7,2%-%timehour: =0%%time:~3,2% 6 | SET startuptasklogsubdir=FixAuth 7 | SET startuptasklogdir=%PathToStartupLogs%\%startuptasklogsubdir%\ 8 | SET startuptasklog=%startuptasklogdir%\startup-%timestamp%.txt 9 | 10 | IF NOT EXIST %startuptasklogdir% MKDIR %startuptasklogdir% 11 | 12 | IF "%ComputeEmulatorRunning%" == "true" ( 13 | ECHO Detected running in an emulator. Skipping the modification of IIS authentication modes. 2>&1 14 | ) ELSE ( 15 | ECHO Starting up %0. >> "%startuptasklog%" 2>&1 16 | 17 | :: For some reason, the authentication changes applied to Web.Config are not working, 18 | :: so these changes need to be applied via appcmd instead. 19 | %systemroot%\system32\inetsrv\appcmd.exe set config -section:anonymousAuthentication -enabled:%ENABLE_ANONYMOUS_AUTHENTICATION% 20 | %systemroot%\system32\inetsrv\appcmd.exe set config -section:windowsAuthentication -enabled:%ENABLE_WINDOWS_AUTHENTICATION% 21 | ) 22 | 23 | :: Log the completion of file/ 24 | ECHO Returned to %0. >> "%startuptasklog%" 2>&1 25 | 26 | IF %ERRORLEVEL% EQU 0 ( 27 | :: No errors occurred. Exit normally. 28 | ECHO Done >> "%startuptasklog%" 29 | EXIT /B 0 30 | ) ELSE ( 31 | :: Log the error. 32 | ECHO An error occurred. The ERRORLEVEL = %ERRORLEVEL%. >> "%startuptasklog%" 2>&1 33 | ECHO Done >> "%startuptasklog%" 34 | EXIT /B %ERRORLEVEL% 35 | ) -------------------------------------------------------------------------------- /AzureService/Profiles/StoreBrokerProxyStaging.azurePubxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {"ServiceManagementEndpoint":"https:\/\/management.core.windows.net\/","ResourceManagementEndpoint":"https:\/\/management.azure.com\/","SubscriptionId":""} 5 | False 6 | StoreBrokerProxy 7 | DeleteAndCreate 8 | Staging 9 | True 10 | False 11 | False 12 | StoreBrokerProxy 13 | StoreBrokerProxy 14 | False 15 | False 16 | Cloud 17 | Release 18 | StoreBrokerProxy 19 | StoreBrokerProxy 20 | True 21 | 22 | -------------------------------------------------------------------------------- /AzureService/Profiles/StoreBrokerProxyProduction.azurePubxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {"ServiceManagementEndpoint":"https:\/\/management.core.windows.net\/","ResourceManagementEndpoint":"https:\/\/management.azure.com\/","SubscriptionId":""} 5 | False 6 | StoreBrokerProxy 7 | DeleteAndCreate 8 | Production 9 | True 10 | False 11 | False 12 | StoreBrokerProxy 13 | StoreBrokerProxy 14 | False 15 | False 16 | Cloud 17 | Release 18 | StoreBrokerProxy 19 | StoreBrokerProxy 20 | True 21 | 22 | -------------------------------------------------------------------------------- /AzureService/ServiceConfiguration.Local.cscfg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /AzureService/ServiceConfiguration.Cloud.cscfg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /StoreBroker.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{F5034706-568F-408A-B7B3-4D38C6DB8A32}") = "StoreBroker", "StoreBroker.pssproj", "{6CAFC0C6-A428-4D30-A9F9-700E829FEA51}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESTProxy", "RESTProxy\RESTProxy.csproj", "{D6FAFB1D-3B56-4332-A76F-E132A1891169}" 9 | EndProject 10 | Project("{CC5FD16D-436D-48AD-A40C-5A424C6E3E79}") = "AzureService", "AzureService\AzureService.ccproj", "{EC8AA218-B675-4003-B2E0-685BD96F8609}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {6CAFC0C6-A428-4D30-A9F9-700E829FEA51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {6CAFC0C6-A428-4D30-A9F9-700E829FEA51}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {6CAFC0C6-A428-4D30-A9F9-700E829FEA51}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {6CAFC0C6-A428-4D30-A9F9-700E829FEA51}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {D6FAFB1D-3B56-4332-A76F-E132A1891169}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {D6FAFB1D-3B56-4332-A76F-E132A1891169}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {D6FAFB1D-3B56-4332-A76F-E132A1891169}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {D6FAFB1D-3B56-4332-A76F-E132A1891169}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {EC8AA218-B675-4003-B2E0-685BD96F8609}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {EC8AA218-B675-4003-B2E0-685BD96F8609}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {EC8AA218-B675-4003-B2E0-685BD96F8609}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {EC8AA218-B675-4003-B2E0-685BD96F8609}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /AzureService/RESTProxyContent/diagnostics.wadcfgx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | true 47 | -------------------------------------------------------------------------------- /AzureService/RESTProxyContent/bin/StartupTasks/RemoteDesktop/Ensure-RemoteDesktopEnabled.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (C) Microsoft Corporation. All rights reserved. 2 | 3 | function Ensure-RegistryKeyProperty 4 | { 5 | <# 6 | .SYNOPSIS 7 | Ensures the registry key property is set with the indicated value. 8 | 9 | .DESCRIPTION 10 | Ensures the registry key property is set with the indicated value. 11 | 12 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 13 | 14 | .PARAMETER RegistryKey 15 | The registry key that has the property that should be set. 16 | 17 | .PARAMETER Property 18 | The name of the property under RegistryKey to set the value for. 19 | 20 | .PARAMETER Value 21 | The new value for Property. 22 | 23 | .PARAMETER Type 24 | The type of registry value that is being set. 25 | 26 | .EXAMPLE 27 | Ensure-RegistryKeyProperty -RegistryKey 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Property 'fDenyTSConnections' -Value 0000 28 | 29 | Sets the DWORD property called fDenyTSConnections to the value 0 under the registry key 30 | SYSTEM\CurrentControlSet\Control\Terminal Server in HKEY_LOCAL_MACHINE. 31 | #> 32 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 33 | param( 34 | [Parameter(Mandatory)] 35 | [string] $RegistryKey, 36 | 37 | [Parameter(Mandatory)] 38 | [string] $Property, 39 | 40 | [Parameter(Mandatory)] 41 | [string] $Value, 42 | 43 | [Microsoft.Win32.RegistryValueKind] $Type = 'DWord' 44 | ) 45 | 46 | process 47 | { 48 | try 49 | { 50 | Set-ItemProperty -Path $RegistryKey -Name $Property -Value $Value -Type $Type -ErrorAction Stop 51 | Write-Verbose "Set property `"$Property`" in registry key `"$RegistryKey`" to `"$Value`" with a type of `"$Type`"" 52 | } 53 | catch 54 | { 55 | Write-Error "Unable to set property $Property in registry key $RegistryKey to $Value" 56 | throw $_ 57 | } 58 | } 59 | } 60 | 61 | function Ensure-RemoteDesktopEnabled 62 | { 63 | <# 64 | .SYNOPSIS 65 | Ensures that Remote Desktop access is enabled. It will always write a value to enable it. 66 | 67 | .DESCRIPTION 68 | Ensures that Remote Desktop access is enabled. It will always write a value to enable it. 69 | 70 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 71 | 72 | .EXAMPLE 73 | Ensure-RemoteDesktopEnabled 74 | 75 | Writes the necessary value to the registry that will enable remote desktop access to 76 | users in the Remote Desktop Users group. 77 | #> 78 | [CmdletBinding()] 79 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 80 | param() 81 | 82 | process 83 | { 84 | Ensure-RegistryKeyProperty -RegistryKey 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Property 'fDenyTSConnections' -Value 0000 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /StoreBroker.pssproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | 2.0 6 | 6CAFC0C6-A428-4d30-A9F9-700E829FEA51 7 | Exe 8 | StoreBroker 9 | StoreBroker 10 | StoreBroker 11 | 12 | 13 | 14 | 15 | true 16 | full 17 | false 18 | bin\Debug\ 19 | DEBUG;TRACE 20 | prompt 21 | 4 22 | 23 | 24 | pdbonly 25 | true 26 | bin\Release\ 27 | TRACE 28 | prompt 29 | 4 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Tests/PackageTool.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Need to split-path $MyInvocation.MyCommand.Path twice and then add StoreBroker folder 2 | $sbModulePath = Join-Path ($MyInvocation.MyCommand.Path | Split-Path -Parent | Split-Path -Parent) "StoreBroker" 3 | 4 | Import-Module $sbModulePath 5 | 6 | InModuleScope StoreBroker { 7 | 8 | Describe "PackageTool" { 9 | 10 | Context "Remove-Comment" { 11 | 12 | It "does not change text with no comments" { 13 | $in = "This is a test string with no comments." 14 | 15 | $in | Remove-Comment | Should BeExactly $in 16 | } 17 | 18 | It "does not change multiple lines of text with no comments" { 19 | $in = @( 20 | "This is a", 21 | "test string split", 22 | "across multiple lines", 23 | "and with no comments" 24 | ) 25 | 26 | $out = $in | Remove-Comment 27 | 28 | # Assert the collection size is the same 29 | $out.Count | Should Be $in.Count 30 | 31 | # Assert the content is the same 32 | foreach ($i in 0..($in.Count - 1)) 33 | { 34 | $out[$i] | Should BeExactly $in[$i] 35 | } 36 | } 37 | 38 | It "removes comments from a single line of text" { 39 | $content = "This is valid content" 40 | $comment = "// this is comment content // and so is this" 41 | $in = $content + $comment 42 | 43 | $in | Remove-Comment | Should BeExactly $content 44 | } 45 | 46 | It "removes comments from multiple lines of text" { 47 | $content = @( 48 | "Here is some ", 49 | "example text ", 50 | "that occurs across", 51 | "multiple lines " 52 | ) 53 | 54 | $comments = @( 55 | "// None of the text ", 56 | "// in this array", 57 | "//is considered", 58 | "// valid." 59 | ) 60 | 61 | $in = @() 62 | foreach ($i in 0..($content.Count - 1)) 63 | { 64 | $in += ($content[$i] + $comments[$i]) 65 | } 66 | 67 | $out = $in | Remove-Comment 68 | 69 | # Assert the collection size is the same 70 | $out.Count | Should Be $content.Count 71 | 72 | # Assert the content is the same 73 | foreach ($i in 0..($in.Count-1)) 74 | { 75 | $out[$i] | Should BeExactly $content[$i] 76 | } 77 | } 78 | 79 | It "removes empty strings and whitespace across multiple lines" { 80 | $in = @( 81 | "", 82 | " ", 83 | " " 84 | ) 85 | 86 | $in | Remove-Comment | Should Be $null 87 | } 88 | 89 | It "returns nothing when all lines are comments" { 90 | $in = @( 91 | "// None of the text ", 92 | "// in this array", 93 | "//is considered", 94 | "// valid." 95 | ) 96 | 97 | $in | Remove-Comment | Should Be $null 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /AzureService/RESTProxyContent/bin/StartupTasks/TimeZone/Ensure-TimeZone.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (C) Microsoft Corporation. All rights reserved. 2 | 3 | function Get-TimeZone 4 | { 5 | <# 6 | .SYNOPSIS 7 | Gets the ID of the device's current timezone. 8 | 9 | .DESCRIPTION 10 | Gets the ID of the device's current timezone. 11 | 12 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 13 | 14 | .EXAMPLE 15 | Get-TimeZone 16 | 17 | Gets the ID of the device's current timezone. 18 | 19 | .OUTPUTS 20 | System.String - The ID of the timezone 21 | #> 22 | [CmdletBinding()] 23 | [OutputType([String])] 24 | param() 25 | 26 | [System.TimeZoneInfo]::ClearCachedData() 27 | $localTimeZone = [system.timezoneinfo]::Local 28 | $localTimeZone.Id 29 | } 30 | 31 | function Set-TimeZone 32 | { 33 | <# 34 | .SYNOPSIS 35 | Changes the device to use the specified timezone. 36 | 37 | .DESCRIPTION 38 | Changes the device to use the specified timezone. 39 | 40 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 41 | 42 | .PARAMETER TimeZoneId 43 | The ID of the timezone that should be used for this device. 44 | 45 | .EXAMPLE 46 | Set-TimeZone -TimeZoneId 'Pacific Standard Time' 47 | 48 | Sets the device to use the PST timezone. 49 | #> 50 | [CmdletBinding(SupportsShouldProcess)] 51 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "", Justification="There doesn't appear to be a better way to change the timezone.")] 52 | param( 53 | [Parameter( 54 | Mandatory, 55 | ValueFromPipeline, 56 | Position=0)] 57 | [string] $TimeZoneId 58 | ) 59 | 60 | if ($PSCmdlet.ShouldProcess($TimeZoneId, "tzutil.exe /s")) 61 | { 62 | Invoke-Expression "tzutil.exe /s ""$TimeZoneId""" -ErrorAction Stop 63 | } 64 | } 65 | 66 | function Ensure-TimeZone 67 | { 68 | <# 69 | .SYNOPSIS 70 | Updates the device's timezone to the specified timezone if it isn't already 71 | using it. 72 | 73 | .DESCRIPTION 74 | Updates the device's timezone to the specified timezone if it isn't already 75 | using it. 76 | 77 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 78 | 79 | .PARAMETER TimeZoneId 80 | The ID of the timezone that should be used for this device. 81 | 82 | .EXAMPLE 83 | Ensure-TimeZone -TimeZoneId 'Pacific Standard Time' 84 | 85 | Sets the device to use the PST timezone if it's not already in PST. 86 | #> 87 | [CmdletBinding()] 88 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 89 | param( 90 | [Parameter( 91 | Mandatory, 92 | ValueFromPipeline, 93 | Position=0)] 94 | [string] $TimeZoneId 95 | ) 96 | 97 | try 98 | { 99 | Write-Verbose "Ensuring that the time zone id is set to ""$TimeZoneId""." 100 | $currentTimeZoneId = Get-TimeZone -ErrorAction Stop; 101 | Write-Verbose "Current time zone id is ""$currentTimeZoneId""." 102 | 103 | if ($currentTimeZoneId -ne $TimeZoneId) 104 | { 105 | Write-Verbose "Time zone ids do not match. Setting time zone to ""$timeZoneId""." 106 | Set-TimeZone $timeZoneId -ErrorAction Stop 107 | Write-Verbose "Successfully set the time zone to ""$timeZoneId""." 108 | } 109 | else 110 | { 111 | Write-Verbose "Current time zone matches expected time zone." 112 | } 113 | } 114 | catch 115 | { 116 | Write-Error "Unable to ensure the time zone is set properly." 117 | throw $_ 118 | } 119 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### VisualStudio ### 2 | ## Ignore Visual Studio temporary files, build results, and 3 | ## files generated by popular Visual Studio add-ons. 4 | 5 | # User-specific files 6 | *.suo 7 | *.user 8 | *.userosscache 9 | *.sln.docstates 10 | 11 | # User-specific files (MonoDevelop/Xamarin Studio) 12 | *.userprefs 13 | 14 | # Build results 15 | [Dd]ebug/ 16 | [Dd]ebugPublic/ 17 | [Rr]elease/ 18 | [Rr]eleases/ 19 | x64/ 20 | x86/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | [Ll]og/ 25 | 26 | # Visual Studio 2015 cache/options directory 27 | .vs/ 28 | # Uncomment if you have tasks that create the project's static files in wwwroot 29 | #wwwroot/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | # DNX 45 | project.lock.json 46 | project.fragment.lock.json 47 | artifacts/ 48 | 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.tmp_proj 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | *.pidb 71 | *.svclog 72 | *.scc 73 | 74 | # Visual C++ cache files 75 | ipch/ 76 | *.aps 77 | *.ncb 78 | *.opendb 79 | *.opensdf 80 | *.sdf 81 | *.cachefile 82 | *.VC.db 83 | *.VC.VC.opendb 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # Web workbench (sass) 103 | .sass-cache/ 104 | 105 | # DocProject is a documentation generator add-in 106 | DocProject/buildhelp/ 107 | DocProject/Help/*.HxT 108 | DocProject/Help/*.HxC 109 | DocProject/Help/*.hhc 110 | DocProject/Help/*.hhk 111 | DocProject/Help/*.hhp 112 | DocProject/Help/Html2 113 | DocProject/Help/html 114 | 115 | # Click-Once directory 116 | publish/ 117 | 118 | # Publish Web Output 119 | *.[Pp]ublish.xml 120 | *.azurePubxml 121 | # TODO: Comment the next line if you want to checkin your web deploy settings 122 | # but database connection strings (with potential passwords) will be unencrypted 123 | *.pubxml 124 | *.publishproj 125 | 126 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 127 | # checkin your Azure Web App publish settings, but sensitive information contained 128 | # in these scripts will be unencrypted 129 | PublishScripts/ 130 | 131 | # NuGet Packages 132 | *.nupkg 133 | # The packages folder can be ignored because of Package Restore 134 | **/packages/* 135 | # except build/, which is used as an MSBuild target. 136 | !**/packages/build/ 137 | # Uncomment if necessary however generally it will be regenerated when needed 138 | #!**/packages/repositories.config 139 | # NuGet v3's project.json files produces more ignoreable files 140 | *.nuget.props 141 | *.nuget.targets 142 | 143 | # Microsoft Azure Build Output 144 | csx/ 145 | *.build.csdef 146 | 147 | # Microsoft Azure Emulator 148 | ecf/ 149 | rcf/ 150 | 151 | # Windows Store app package directories and files 152 | AppPackages/ 153 | BundleArtifacts/ 154 | Package.StoreAssociation.xml 155 | _pkginfo.txt 156 | 157 | # Visual Studio cache files 158 | # files ending in .cache can be ignored 159 | *.[Cc]ache 160 | # but keep track of directories ending in .cache 161 | !*.[Cc]ache/ 162 | 163 | # Others 164 | ClientBin/ 165 | ~$* 166 | *~ 167 | *.dbmdl 168 | *.dbproj.schemaview 169 | *.pfx 170 | *.publishsettings 171 | node_modules/ 172 | orleans.codegen.cs 173 | 174 | # RIA/Silverlight projects 175 | Generated_Code/ 176 | 177 | # Backup & report files from converting an old project file 178 | # to a newer Visual Studio version. Backup files are not needed, 179 | # because we have git ;-) 180 | _UpgradeReport_Files/ 181 | Backup*/ 182 | UpgradeLog*.XML 183 | UpgradeLog*.htm 184 | 185 | # Node.js Tools for Visual Studio 186 | .ntvs_analysis.dat 187 | 188 | # Visual Studio 6 build log 189 | *.plg 190 | 191 | # Visual Studio 6 workspace options file 192 | *.opt -------------------------------------------------------------------------------- /PDP/InAppProductDescription.xsd: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Root element of InAppProductDescription document type. For content guidance on all required and optional fields in the product description, see http://go.microsoft.com/fwlink/p/?linkid=221134. 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | Required, the title for the add-on listing. May not exceed 100 characters. 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | Optional, the description for the add-on listing. May not exceed 200 characters. 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | Optional, the icon for the listing. 100 | The icon should be a 300 x 300 png file. 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /AzureService/RESTProxyContent/bin/StartupTasks/Helpers/GroupManagement.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (C) Microsoft Corporation. All rights reserved. 2 | 3 | function Ensure-IsInGroup 4 | { 5 | <# 6 | .SYNOPSIS 7 | Ensures the user is in the specified group. If not, it will add them. 8 | 9 | .DESCRIPTION 10 | Ensures the user is in the specified group. If not, it will add them. 11 | 12 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 13 | 14 | .PARAMETER UserName 15 | The user that should be in Group 16 | 17 | .PARAMETER Group 18 | The group that UserName should be a member of. 19 | 20 | .PARAMETER Domain 21 | The domain for UserName (if relevant) 22 | 23 | .EXAMPLE 24 | Ensure-IsInGroup -UserName "myUser" -Group "Administrators" 25 | 26 | Checks to see if the user "myUser" is in the Administrators group. If not, adds them. 27 | #> 28 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 29 | param( 30 | [Parameter(Mandatory)] 31 | [string] $UserName, 32 | 33 | [Parameter(Mandatory)] 34 | [string] $Group, 35 | 36 | [string] $Domain = $null 37 | ) 38 | 39 | process 40 | { 41 | Write-Verbose "Ensuring user [$UserName] is a member of the [$Group] group." 42 | 43 | # ADSI == Active Directory Service Interface 44 | $groupAdsi = [ADSI]"WinNT://${env:Computername}/$Group, group" 45 | $members = @($groupAdsi.psbase.Invoke("Members")) | ForEach-Object { ([ADSI]$_).InvokeGet("Name") } 46 | 47 | if ($members -notcontains $UserName) 48 | { 49 | Write-Verbose "User [$UserName] is not a member of the [$Group] group. They will be added." 50 | 51 | try 52 | { 53 | if ([string]::IsNullOrEmpty($Domain)) 54 | { 55 | $groupAdsi.Add("WinNT://${env:Computername}/$UserName, user") 56 | } 57 | else 58 | { 59 | $groupAdsi.psbase.Invoke("Add", ([ADSI]"WinNT://$Domain/$UserName").path) 60 | } 61 | 62 | Write-Verbose "User [$UserName] was added to the [$Group] group." 63 | } 64 | catch 65 | { 66 | Write-Error "Unable to add user [$UserName] to the [$Group] group." 67 | throw $_ 68 | } 69 | } 70 | else 71 | { 72 | Write-Verbose "User [$UserName] is already a member of the [$Group] group." 73 | } 74 | 75 | } 76 | } 77 | 78 | function Ensure-IsInAdministratorsGroup 79 | { 80 | <# 81 | .SYNOPSIS 82 | Ensures the user is in the Administrators group. If not, it will add them. 83 | 84 | .DESCRIPTION 85 | Ensures the user is in the Administrators group. If not, it will add them. 86 | 87 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 88 | 89 | .PARAMETER UserName 90 | The user that should be in Administrators group 91 | 92 | .PARAMETER Domain 93 | The domain for UserName (if relevant) 94 | 95 | .EXAMPLE 96 | Ensure-IsInAdministratorsGroup "myUser" 97 | 98 | Checks to see if the user "myUser" is in the Administrators group. If not, adds them. 99 | #> 100 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 101 | param( 102 | [Parameter(Mandatory)] 103 | [string] $UserName, 104 | 105 | [string] $Domain = $null 106 | ) 107 | 108 | process 109 | { 110 | Ensure-IsInGroup -UserName $UserName -Group 'Administrators' -Domain $Domain 111 | } 112 | } 113 | 114 | function Ensure-IsInRemoteDesktopUsersGroup 115 | { 116 | <# 117 | .SYNOPSIS 118 | Ensures the user is in the Remote Desktop Users group. if not, it will add them. 119 | 120 | .DESCRIPTION 121 | Ensures the user is in the Remote Desktop Users group. if not, it will add them. 122 | 123 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 124 | 125 | .PARAMETER UserName 126 | The user that should be in Remote Desktop Users group 127 | 128 | .PARAMETER Domain 129 | The domain for UserName (if relevant) 130 | 131 | .EXAMPLE 132 | Ensure-IsInRemoteDesktopUsersGroup "myUser" 133 | 134 | Checks to see if the user "myUser" is in the Remote Desktop Users group. If not, adds them. 135 | #> 136 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 137 | param( 138 | [Parameter(Mandatory)] 139 | [string] $UserName, 140 | 141 | [string] $Domain = $null 142 | ) 143 | 144 | process 145 | { 146 | Ensure-IsInGroup -UserName $UserName -Group 'Remote Desktop Users' -Domain $Domain 147 | } 148 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StoreBroker PowerShell Module 2 | 3 | [![powershellgallery](https://img.shields.io/powershellgallery/v/StoreBroker)](https://www.powershellgallery.com/packages/StoreBroker) 4 | [![nuget](https://img.shields.io/nuget/v/Microsoft.Windows.StoreBroker)](https://www.nuget.org/packages/Microsoft.Windows.StoreBroker/) 5 | 6 | #### Table of Contents 7 | 8 | * [Overview](#overview) 9 | * [Project Status](#project-status) 10 | * [Goals](#goals) 11 | * [Current Functionality](#current-functionality) 12 | * [Prerequisites](#prerequisites) 13 | * [What's New](#whats-new) 14 | * [Installation and Setup](#installation-and-setup) 15 | * [Usage](#usage) 16 | * [Developing and Contributing](#developing-and-contributing) 17 | * [Legal and Licensing](#legal-and-licensing) 18 | * [Governance](#governance) 19 | * [Code of Conduct](#code-of-conduct) 20 | * [Privacy Policy](#privacy-policy) 21 | * [Terms of Use](#terms-of-use) 22 | 23 | ---------- 24 | 25 | ## Overview 26 | 27 | StoreBroker is a [PowerShell](https://microsoft.com/powershell) [module](https://technet.microsoft.com/en-us/library/dd901839.aspx) 28 | that provides command-line interaction and automation for Application and In-App Purchase 29 | submissions to the Windows Store via the 30 | [Windows Store Submission API](https://msdn.microsoft.com/windows/uwp/monetize/create-and-manage-submissions-using-windows-store-services). 31 | 32 | It's designed for all Windows developers that publish their apps into the Windows Store. 33 | 34 | Most Microsoft-published applications in the Windows Store have metadata (screenshots, captions, 35 | descriptions, feature lists, etc...) localized across 64 languages; updating all of these values 36 | manually through the [Developer Portal](https://dev.windows.com) can take hours (or days). 37 | StoreBroker is able to complete a full metadata update in a matter of minutes with just two 38 | commands, and that's just the start of what it can do. 39 | 40 | ### Project Status 41 | 42 | **Production** 43 | 44 | This module is **actively used within Microsoft** as the _primary way_ that our first-party 45 | application teams submit flight updates and public submission updates to the Windows Store, 46 | and is now available to the general developer community to leverage as well. 47 | 48 | ### Goals 49 | 50 | **StoreBroker** should ... 51 | 52 | ... have full support for exercising every aspect of the Windows Submission API. 53 | 54 | ... be easy to use, and consistent in its functionality. 55 | 56 | ... save developer time and decrease the friction with getting updates to the Windows Store. 57 | 58 | ### Current Functionality 59 | 60 | At a very high level, this is what you can do with StoreBroker: 61 | 62 | - App Submissions 63 | - Retrieve / Create / Update / Remove 64 | 65 | - Flights 66 | - Retrieve / Create / Update / Remove 67 | 68 | - Flight Submissions 69 | - Retrieve / Create / Update / Remove 70 | 71 | - In-App Products (IAP's) 72 | - Retrieve / Create / Update / Remove 73 | 74 | - In-App Product (IAP) Submissions 75 | - Retrieve / Create / Update / Remove 76 | 77 | - General 78 | - Submission Monitor with email support 79 | 80 | ### Prerequisites 81 | 82 | This module requires PowerShell [version 4](https://en.wikipedia.org/wiki/PowerShell#PowerShell_4.0) 83 | or higher. 84 | 85 | [More prerequisites](Documentation/SETUP.md#prerequisites) are covered in 86 | [SETUP.md](Documentation/SETUP.md#prerequisites). 87 | 88 | ---------- 89 | 90 | ## What's New 91 | 92 | Check out [CHANGELOG.md](./CHANGELOG.md) to review the details of the current release as well as 93 | all past releases. 94 | 95 | ---------- 96 | 97 | ## Installation and Setup 98 | 99 | Refer to [SETUP.md](Documentation/SETUP.md) for the Installation and Setup instructions. 100 | 101 | ---------- 102 | 103 | ## Usage 104 | 105 | Refer to [USAGE.md](Documentation/USAGE.md) for usage information. 106 | 107 | ---------- 108 | 109 | ## Developing and Contributing 110 | 111 | Please see the [Contribution Guide](CONTRIBUTING.md) for information on how to develop and 112 | contribute. 113 | 114 | If you have any problems, please consult [GitHub Issues](https://github.com/Microsoft/StoreBroker/issues), 115 | and the [FAQ](Documentation/USAGE.md#faq). 116 | 117 | If you do not see your problem captured, please file [feedback](CONTRIBUTING.md#feedback). 118 | 119 | ---------- 120 | 121 | ## Legal and Licensing 122 | 123 | StoreBroker is licensed under the [MIT license](LICENSE). 124 | 125 | ------------------- 126 | 127 | ## Governance 128 | 129 | Governance policy for the StoreBroker project is described [here](Documentation/GOVERNANCE.md). 130 | 131 | ---------- 132 | 133 | ## Code of Conduct 134 | 135 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 136 | For more information see the [Code of conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 137 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions 138 | or comments. 139 | 140 | ---------- 141 | 142 | ## Privacy Policy 143 | 144 | For more information, refer to Microsoft's [Privacy Policy](https://go.microsoft.com/fwlink/?LinkID=521839). 145 | 146 | ---------- 147 | 148 | ## Terms of Use 149 | 150 | For more information, refer to Microsoft's [Terms of Use](https://www.microsoft.com/en-us/legal/intellectualproperty/copyright/default.aspx). 151 | -------------------------------------------------------------------------------- /AzureService/AzureService.ccproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 2.9 8 | ec8aa218-b675-4003-b2e0-685bd96f8609 9 | Library 10 | Properties 11 | AzureService 12 | AzureService 13 | True 14 | AzureService 15 | False 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | RESTProxy 43 | {d6fafb1d-3b56-4332-a76f-e132a1891169} 44 | True 45 | Web 46 | RESTProxy 47 | True 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | Content 72 | 73 | 74 | Content 75 | 76 | 77 | Content 78 | 79 | 80 | Content 81 | 82 | 83 | Content 84 | 85 | 86 | Content 87 | 88 | 89 | Content 90 | 91 | 92 | Content 93 | 94 | 95 | Content 96 | 97 | 98 | Content 99 | 100 | 101 | Content 102 | 103 | 104 | 105 | 106 | 10.0 107 | $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Windows Azure Tools\2.9\ 108 | 109 | 110 | -------------------------------------------------------------------------------- /AzureService/RESTProxyContent/bin/StartupTasks/LocalAdmin/Ensure-LocalAdmin.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (C) Microsoft Corporation. All rights reserved. 2 | 3 | # Load the helper cmdlets 4 | . $PSScriptRoot\..\Helpers\Encryption.ps1 5 | . $PSScriptRoot\..\Helpers\GroupManagement.ps1 6 | 7 | function Ensure-LocalUserExists 8 | { 9 | <# 10 | .SYNOPSIS 11 | Ensures the local user exists. If it doesn't, it will create it. 12 | 13 | .DESCRIPTION 14 | Ensures the local user exists. If it doesn't, it will create it. 15 | 16 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 17 | 18 | .PARAMETER UserName 19 | The name of the local user to ensure exists. 20 | 21 | .PARAMETER Secret 22 | The Base64 encrypted password for UserName. 23 | 24 | .PARAMETER CertThumbprint 25 | The thumbprint (40-digit hex number) of the certificate to use to decode Secret. 26 | 27 | .PARAMETER CertStore 28 | The certificate store that the certificate with CertThumbrint can be found. 29 | 30 | .EXAMPLE 31 | Ensure-LocalUserExists -UserName "myUser" -Secret $encrypted -CertThumbrint 1234567890ABCDEF1234567890ABCDEF12345678 32 | 33 | Will ensure that the current computer has a local user named "myUser" with a password that 34 | is found after decrypting $encrypted using the certificate found with the specified 35 | thumbprint in the local computer certificate store. 36 | #> 37 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 38 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Exists isn't plural in this instance.")] 39 | param( 40 | [Parameter(Mandatory)] 41 | [string] $UserName, 42 | 43 | [Parameter(Mandatory)] 44 | [string] $Secret, 45 | 46 | [Parameter(Mandatory)] 47 | [string] $CertThumbprint, 48 | 49 | [string] $CertStore = "cert:\LocalMachine\My" 50 | ) 51 | process 52 | { 53 | Write-Verbose "Ensuring user [$UserName] exists" 54 | 55 | # ADSI == Active Directory Service Interface 56 | $computerAdsi = [ADSI]"WinNT://${env:Computername}" 57 | $localUsers = $computerAdsi.Children | 58 | Where-Object { $_.SchemaClassName -eq 'user' } | 59 | ForEach-Object { $_.name[0].ToString() } 60 | 61 | if ($localUsers -notcontains $UserName) 62 | { 63 | Write-Verbose "User [$UserName] does not exit. It will be created." 64 | 65 | try 66 | { 67 | Write-Verbose "Decrypting password." 68 | $password = Decrypt-Asymmetric -EncryptedBase64String $Secret -CertThumbprint $CertThumbprint -CertStore $CertStore -ErrorAction Stop 69 | 70 | Write-Verbose "Password decrypted." 71 | $user = $computerAdsi.Create("User", $UserName) 72 | $user.setPassword($password) 73 | $user.SetInfo(); 74 | $user.description = "Local Admin created via Cloud Service startup script." 75 | $user.SetInfo(); 76 | 77 | Write-Verbose "Created user [$UserName]" 78 | } 79 | catch 80 | { 81 | Write-Error "Unable to create user [$UserName]" 82 | Throw $_ 83 | } 84 | } 85 | else 86 | { 87 | Write-Verbose "User [$UserName] already exists." 88 | } 89 | } 90 | 91 | } 92 | 93 | function Ensure-LocalAdmin 94 | { 95 | <# 96 | .SYNOPSIS 97 | Ensures the specified user exists and is an Administrator with RDP access. 98 | 99 | .DESCRIPTION 100 | Ensures the specified user exists and is an Administrator with RDP access. 101 | 102 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 103 | 104 | .PARAMETER UserName 105 | The name of the local user that should be an administrator with RDP access. 106 | 107 | .PARAMETER Secret 108 | The Base64 encrypted password for UserName. 109 | 110 | .PARAMETER CertThumbprint 111 | The thumbprint (40-digit hex number) of the certificate to use to decode Secret. 112 | 113 | .PARAMETER CertStore 114 | The certificate store that the certificate with CertThumbrint can be found. 115 | 116 | .EXAMPLE 117 | Ensure-LocalAdmin -UserName "myUser" -Secret $encrypted -CertThumbrint 1234567890ABCDEF1234567890ABCDEF12345678 118 | 119 | Will ensure that the current computer has a local user named "myUser" with a password that 120 | is found after decrypting $encrypted using the certificate found with the specified 121 | thumbprint in the local computer certificate store. That user will be a member of the 122 | Administrators group and the Remote Desktop Users group. 123 | #> 124 | [CmdletBinding()] 125 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 126 | param( 127 | [Parameter(Mandatory)] 128 | [string] $UserName, 129 | 130 | [Parameter(Mandatory)] 131 | [string] $Secret, 132 | 133 | [Parameter(Mandatory)] 134 | [string] $CertThumbprint, 135 | 136 | [string] $CertStore = "cert:\LocalMachine\My" 137 | ) 138 | 139 | process 140 | { 141 | Ensure-LocalUserExists -UserName $UserName -Secret $Secret -CertThumbprint $CertThumbprint -CertStore $CertStore 142 | Ensure-IsInAdministratorsGroup -UserName $UserName 143 | Ensure-IsInRemoteDesktopUsersGroup -UserName $UserName 144 | } 145 | } -------------------------------------------------------------------------------- /AzureService/ServiceDefinition.csdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /StoreBroker/StoreBroker.psd1: -------------------------------------------------------------------------------- 1 | # Copyright (C) Microsoft Corporation. All rights reserved. 2 | 3 | @{ 4 | GUID = '10d324f0-4333-4ef7-9e85-93b7fc83f5fb' 5 | Author = 'Microsoft Corporation' 6 | CompanyName = 'Microsoft Corporation' 7 | Copyright = 'Copyright (C) Microsoft Corporation. All rights reserved.' 8 | 9 | ModuleVersion = '1.21.24' 10 | Description = 'Provides command-line access to the Windows Store Submission REST API.' 11 | 12 | RootModule = 'StoreIngestionApi' 13 | 14 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 15 | NestedModules = @( 16 | 'Helpers.ps1', 17 | 'NugetTools.ps1', 18 | 'PackageTool.ps1', 19 | 'StoreIngestionApplicationApi.ps1', 20 | 'StoreIngestionIapApi.ps1', 21 | 'StoreIngestionFlightingApi.ps1', 22 | 'Telemetry.ps1') 23 | 24 | # Minimum version of the Windows PowerShell engine required by this module 25 | PowerShellVersion = '4.0' 26 | 27 | FunctionsToExport = @( 28 | 'Clear-StoreBrokerAuthentication', 29 | 'Complete-ApplicationFlightSubmission', 30 | 'Complete-ApplicationFlightSubmissionPackageRollout', 31 | 'Complete-ApplicationSubmission', 32 | 'Complete-ApplicationSubmissionPackageRollout', 33 | 'Complete-InAppProductSubmission', 34 | 'Format-Application', 35 | 'Format-ApplicationFlight', 36 | 'Format-ApplicationFlightSubmission', 37 | 'Format-ApplicationFlights', 38 | 'Format-ApplicationInAppProducts', 39 | 'Format-ApplicationSubmission', 40 | 'Format-Applications', 41 | 'Format-InAppProduct', 42 | 'Format-InAppProductSubmission', 43 | 'Format-InAppProducts', 44 | 'Get-Application', 45 | 'Get-ApplicationFlight', 46 | 'Get-ApplicationFlightSubmission', 47 | 'Get-ApplicationFlightSubmissionPackageRollout', 48 | 'Get-ApplicationFlightSubmissionStatus', 49 | 'Get-ApplicationFlights', 50 | 'Get-ApplicationInAppProducts', 51 | 'Get-ApplicationSubmission', 52 | 'Get-ApplicationSubmissionPackageRollout', 53 | 'Get-ApplicationSubmissionStatus', 54 | 'Get-Applications', 55 | 'Get-FlightGroups', 56 | 'Get-InAppProduct', 57 | 'Get-InAppProductSubmission', 58 | 'Get-InAppProductSubmissionStatus', 59 | 'Get-InAppProducts', 60 | 'Get-SubmissionPackage', 61 | 'Invoke-SBRestMethod', 62 | 'Invoke-SBRestMethodMultipleResult', 63 | 'Join-SubmissionPackage', 64 | 'New-ApplicationFlight', 65 | 'New-ApplicationFlightSubmission', 66 | 'New-ApplicationSubmission', 67 | 'New-InAppProductSubmission', 68 | 'New-InAppProductSubmissionPackage', 69 | 'New-InAppProduct', 70 | 'New-StoreBrokerConfigFile', 71 | 'New-StoreBrokerInAppProductConfigFile', 72 | 'New-SubmissionPackage', 73 | 'Open-DevPortal', 74 | 'Open-Store', 75 | 'Remove-ApplicationFlight', 76 | 'Remove-ApplicationFlightSubmission', 77 | 'Remove-ApplicationSubmission', 78 | 'Remove-InAppProduct', 79 | 'Remove-InAppProductSubmission', 80 | 'Set-ApplicationFlightSubmission', 81 | 'Set-ApplicationSubmission', 82 | 'Set-InAppProductSubmission', 83 | 'Set-StoreBrokerAuthentication', 84 | 'Set-SubmissionPackage', 85 | 'Start-ApplicationFlightSubmissionMonitor', 86 | 'Start-InAppProductSubmissionMonitor', 87 | 'Start-SubmissionMonitor', 88 | 'Stop-ApplicationFlightSubmissionPackageRollout', 89 | 'Stop-ApplicationSubmissionPackageRollout', 90 | 'Update-ApplicationFlightSubmission', 91 | 'Update-ApplicationFlightSubmissionPackageRollout', 92 | 'Update-ApplicationSubmission', 93 | 'Update-ApplicationSubmissionPackageRollout', 94 | 'Update-InAppProductSubmission') 95 | 96 | AliasesToExport = @( 97 | 'Commit-ApplicationFlightSubmission', 98 | 'Commit-ApplicationSubmission', 99 | 'Commit-IapSubmission', 100 | 'Commit-InAppProductSubmission', 101 | 'Complete-InAppProductSubmission', 102 | 'Finalize-ApplicationFlightSubmissionPackageRollout', 103 | 'Finalize-ApplicationSubmissionPackageRollout', 104 | 'Format-ApplicationIaps', 105 | 'Format-Iap', 106 | 'Format-IapSubmission', 107 | 'Format-Iaps', 108 | 'Get-ApplicationIaps', 109 | 'Get-Iap', 110 | 'Get-IapSubmission', 111 | 'Get-IapSubmissionStatus', 112 | 'Get-Iaps', 113 | 'Halt-ApplicationFlightSubmissionPackageRollout', 114 | 'Halt-ApplicationSubmissionPackageRollout', 115 | 'New-Iap', 116 | 'New-IapSubmission', 117 | 'New-IapSubmissionPackage', 118 | 'New-PackageToolConfigFile', 119 | 'New-StoreBrokerIapConfigFile', 120 | 'Remove-Iap', 121 | 'Remove-IapSubmission', 122 | 'Replace-ApplicationFlightSubmission', 123 | 'Replace-ApplicationSubmission', 124 | 'Replace-IapSubmission', 125 | 'Replace-InAppProductSubmission', 126 | 'Set-IapSubmission', 127 | 'Set-SubmissionPackage', 128 | 'Start-ApplicationSubmissionMonitor', 129 | 'Start-IapSubmissionMonitor', 130 | 'Update-IapSubmission', 131 | 'Upload-ApplicationSubmissionPackage', 132 | 'Upload-SubmissionPackage') 133 | 134 | #CmdletsToExport = '*' 135 | 136 | #VariablesToExport = '*' 137 | 138 | # Private data to pass to the module specified in RootModule/ModuleToProcess. 139 | PrivateData = @{ 140 | # Hashtable with additional module metadata used by PowerShell. 141 | PSData = @{ 142 | # Tags applied to this module. These help with module discovery in online galleries. 143 | Tags = @('Store', 'App', 'Submission') 144 | 145 | # A URL to the license for this module. 146 | LicenseUri = 'https://aka.ms/StoreBroker_License' 147 | 148 | # A URL to the main website for this project. 149 | ProjectUri = 'https://aka.ms/StoreBroker' 150 | 151 | # A URL to an icon representing this module. 152 | # IconUri = '' 153 | 154 | # ReleaseNotes of this module 155 | # ReleaseNotes = '' 156 | } 157 | } 158 | 159 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 160 | # DefaultCommandPrefix = 'SB' 161 | 162 | # Modules that must be imported into the global environment prior to importing this module 163 | # RequiredModules = @() 164 | 165 | # Assemblies that must be loaded prior to importing this module 166 | # RequiredAssemblies = @() 167 | 168 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 169 | # ScriptsToProcess = @() 170 | 171 | # List of all modules packaged with this module 172 | # ModuleList = @() 173 | 174 | # List of all files packaged with this module 175 | # FileList = @() 176 | 177 | # HelpInfo URI of this module 178 | # HelpInfoURI = '' 179 | } 180 | -------------------------------------------------------------------------------- /Documentation/GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # StoreBroker PowerShell Module 2 | ## Governance 3 | 4 | ## Terms 5 | 6 | * [**StoreBroker Committee**](#storebroker-committee): A committee of project owners who are 7 | responsible for design decisions, and approving new maintainers/committee members. 8 | 9 | * **Project Leads**: Project Leads support the StoreBroker Committee, engineering teams, and 10 | community by working across Microsoft teams and leadership, and working through issues with 11 | other companies. The initial Project Leads for StoreBroker are 12 | Howard Wolosky ([HowardWolosky-MSFT](https://github.com/HowardWolosky-MSFT)) and 13 | Daniel Belcher ([DanBelcher-MSFT](https://github.com/DanBelcher-MSFT)). 14 | 15 | * [**Repository maintainer**](#repository-maintainers): An individual responsible for merging 16 | pull requests (PRs) into `master` when all requirements are met (code review, tests, docs, 17 | as applicable). Repository Maintainers are the only people with write permissions into `master`. 18 | 19 | * [**Area experts**](#area-experts): People who are experts for specific components or 20 | technologies (e.g. security, performance). Area experts are responsible for code reviews, 21 | issue triage, and providing their expertise to others. 22 | 23 | * **Corporation**: The Corporation owns the StoreBroker repository and, under extreme circumstances, 24 | reserves the right to dissolve or reform the StoreBroker Committee, the Project Leads, and the 25 | Corporate Maintainer. The Corporation for StoreBroker is Microsoft. 26 | 27 | * **Corporate Maintainer**: The Corporate Maintainer is an entity, person or set of persons, with 28 | the ability to veto decisions made by the StoreBroker Committee or any other collaborators on the 29 | StoreBroker project. This veto power will be used with restraint since it is intended that the 30 | community drive the project. The Corporate Maintainer is determined by the Corporation both 31 | initially and in continuation. The initial Corporate Maintainer for StoreBroker is 32 | Howard Wolosky ([HowardWolosky-MSFT](https://github.com/HowardWolosky-MSFT)). 33 | 34 | ## StoreBroker Committee 35 | 36 | The StoreBroker Committee and its members (aka Committee Members) are the primary caretakers of 37 | StoreBroker. 38 | 39 | ### Current Committee Members 40 | 41 | * Howard Wolosky ([@HowardWolosky](https://github.com/HowardWolosky)) 42 | * Daniel Belcher ([@DanBelcher-MSFT](https://github.com/DanBelcher-MSFT)). 43 | 44 | ### Committee Member Responsibilities 45 | 46 | Committee Members are responsible for reviewing and approving proposed new features or design changes. 47 | 48 | #### Committee Member DOs and DON'Ts 49 | 50 | As a StoreBroker Committee Member: 51 | 52 | 1. **DO** reply to issues and pull requests with design opinions 53 | (this could include offering support for good work or exciting new features) 54 | 2. **DO** encourage healthy discussion about the direction of StoreBroker 55 | 3. **DO** raise "red flags" on PRs that require a larger design discussion 56 | 4. **DO** contribute to documentation and best practices 57 | 5. **DO** maintain a presence in the StoreBroker community outside of GitHub 58 | (Twitter, blogs, StackOverflow, Reddit, Hacker News, etc.) 59 | 6. **DO** heavily incorporate community feedback into the weight of your decisions 60 | 7. **DO** be polite and respectful to a wide variety of opinions and perspectives 61 | 8. **DO** make sure contributors are following the [contributor guidelines](../CONTRIBUTING.md) 62 | 9. **DON'T** constantly raise "red flags" for unimportant or minor problems to the point that the 63 | progress of the project is being slowed 64 | 10. **DON'T** offer up your opinions as the absolute opinion of the StoreBroker Committee. 65 | Members are encouraged to share their opinions, but they should be presented as such. 66 | 67 | ### StoreBroker Committee Membership 68 | 69 | The initial StoreBroker Committee consists of Microsoft employees. 70 | It is expected that over time, StoreBroker experts in the community will be made Committee Members. 71 | Membership is heavily dependent on the level of contribution and expertise: 72 | individuals who contribute in meaningful ways to the project will be recognized accordingly. 73 | 74 | At any point in time, a Committee Member can nominate a strong community member to join the Committee. 75 | Nominations should be submitted in the form of an `Issue` with the `committee nomination` label 76 | detailing why that individual is qualified and how they will contribute. After the `Issue` has 77 | been discussed, a unanimous vote will be required for the new Committee Member to be confirmed. 78 | 79 | ## Repository Maintainers 80 | 81 | Repository Maintainers are trusted stewards of the StoreBroker repository responsible for 82 | maintaining consistency and quality of the code. 83 | 84 | One of their primary responsibilities is merging pull requests after all requirements have been 85 | fulfilled. 86 | 87 | ## Area Experts 88 | 89 | Area Experts are people with knowledge of specific components or technologies in the StoreBroker 90 | domain. They are responsible for code reviews, issue triage, and providing their expertise to others. 91 | 92 | They have [write access](https://help.github.com/articles/repository-permission-levels-for-an-organization/) 93 | to the StoreBroker repository which gives them the power to: 94 | 95 | 1. `git push` to all branches *except* `master`. 96 | 2. Merge pull requests to all branches *except* `master` (though this should not be common 97 | given that `master` is the only long-living branch. 98 | 3. Assign labels, milestones, and people to [issues](https://guides.github.com/features/issues/). 99 | 100 | ### Area Expert Responsibilities 101 | 102 | If you are an Area Expert, you are expected to be actively involved in any development, design, or contributions in your area of expertise. 103 | 104 | If you are an Area Expert: 105 | 106 | 1. **DO** assign the correct labels 107 | 2. **DO** assign yourself to issues labeled with your area of expertise 108 | 3. **DO** code reviews for issues where you're assigned or in your areas of expertise. 109 | 4. **DO** reply to new issues and pull requests that are related to your area of expertise 110 | (while reviewing PRs, leave your comment even if everything looks good - a simple 111 | "Looks good to me" or "LGTM" will suffice, so that we know someone has already taken a look at it). 112 | 5. **DO** make sure contributors are following the [contributor guidelines](../CONTRIBUTING.md). 113 | 6. **DO** ask people to resend a pull request, if it doesn't target `master`. 114 | 7. **DO** ensure that contributors [write Pester tests](../CONTRIBUTING.md#testing) for all 115 | new/changed functionality when possible. 116 | 8. **DO** ensure that contributors write documentation for all new-/changed functionality 117 | 9. **DO** encourage contributors to refer to issues in their pull request description 118 | (e.g. `Resolves issue #123`). 119 | 10. **DO** encourage contributors to create meaningful titles for all PRs. Edit title if necessary. 120 | 11. **DO** verify that all contributors are following the [Coding Guidelines](../CONTRIBUTING.md#coding-guidelines). 121 | 12. **DON'T** create new features, new designs, or change behaviors without having a public 122 | discussion on the design within its Issue. -------------------------------------------------------------------------------- /AzureService/RESTProxyContent/bin/StartupTasks/Helpers/Encryption.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (C) Microsoft Corporation. All rights reserved. 2 | 3 | Function Encrypt-Asymmetric 4 | { 5 | <# 6 | .SYNOPSIS 7 | Encrypts a string using the public key on a certificate. 8 | 9 | .DESCRIPTION 10 | Encrypts a string using the public key on a certificate. 11 | 12 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 13 | 14 | .PARAMETER ClearText 15 | The text to encrypt. 16 | 17 | .PARAMETER PublicCertFilePath 18 | The path to the certificate file that has the public key to use for the encyption process. 19 | 20 | .EXAMPLE 21 | $encryptedText = Encrypt-Asymmetric "hello world" c:\certs\mycert.cer 22 | 23 | $encryptedText will contain the base64 encoded, encrypted version of 24 | the string "hello world", using the public key that is defined in the 25 | c:\certs\mycert.cer certificate. 26 | 27 | .OUTPUTS 28 | System.String - The encrypted string using Base64 encoding. 29 | #> 30 | [CmdletBinding()] 31 | [OutputType([string])] 32 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 33 | param( 34 | [Parameter( 35 | Mandatory=$true, 36 | Position=0)] 37 | [ValidateNotNullOrEmpty()] 38 | [string] $ClearText, 39 | 40 | [Parameter( 41 | Mandatory=$true, 42 | Position=1)] 43 | [ValidateNotNullOrEmpty()] 44 | [ValidateScript({Test-Path $_ -PathType Leaf})] 45 | [string] $PublicCertFilePath 46 | ) 47 | 48 | # Encrypts a string with a public key 49 | $PublicCertFilePath = Resolve-Path -Path $PublicCertFilePath 50 | $publicCert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $PublicCertFilePath 51 | $byteArray = [System.Text.Encoding]::UTF8.GetBytes($ClearText) 52 | $encryptedByteArray = $publicCert.PublicKey.Key.Encrypt($byteArray,$true) 53 | $base64String = [Convert]::ToBase64String($encryptedByteArray) 54 | 55 | return $base64String 56 | } 57 | 58 | 59 | Function Decrypt-Asymmetric 60 | { 61 | <# 62 | .SYNOPSIS 63 | Decrypts a string using the private key of a certificate. 64 | 65 | .DESCRIPTION 66 | Decrypts a string using the private key of a certificate. 67 | 68 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 69 | 70 | .PARAMETER EncryptedBase64String 71 | The encrypted string to decrypt. 72 | 73 | .PARAMETER CertThumbprint 74 | The thumbprint (40-digit hex number) of the certificate to use. 75 | Used to find the correct certificate in the certificate store. 76 | 77 | .PARAMETER CertStore 78 | The certificate store to search for a certificate matching CertThumbprint 79 | to use for the decryption of EncryptedBase64String. 80 | 81 | .PARAMETER PrivateCertPath 82 | The path to the private certificate to use for decryption, to be used instead of searching 83 | for the certificate by thumbprint in a given certificate store. 84 | 85 | .PARAMETER PrivateCertPassword 86 | The password for the certificate defined by PrivateCertPath so that the decryption process 87 | can occur. 88 | 89 | .EXAMPLE 90 | $clearText = Decrypt-Asymmetric -EncryptedBase64String $encryptedText -CertThumbprint 1234567890ABCDEF1234567890ABCDEF12345678 -CertStore "cert:\LocalMachine\My" 91 | 92 | $clearText will contain the decrypted version of $decryptedText, by looking up the 93 | certificate with the specified thumbrint in the local computer's certificate store for 94 | the private key. 95 | 96 | .EXAMPLE 97 | $clearText = Decrypt-Asymmetric -EncryptedBase64String $encryptedText -PrivateCertPath "c:\cert\MyCert.pfx" -PrivateCertPassword "myPa$$word" 98 | 99 | $clearText will contain the decrypted version of $decryptedText, by applying the 100 | specified password to the specified certiticate for decryption to occur. 101 | 102 | .OUTPUTS 103 | System.String - The encrypted string using Base64 encoding. 104 | #> 105 | [CmdletBinding()] 106 | [OutputType([string])] 107 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 108 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "", Justification="This is the explicit purpose of this method")] 109 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "", Justification="Caller would be dealing in a plain text password in this scenario.")] 110 | param( 111 | [Parameter( 112 | ParameterSetName='store', 113 | Position=0, 114 | Mandatory=$true)] 115 | [Parameter( 116 | ParameterSetName='file', 117 | Position=0, 118 | Mandatory=$true)] 119 | [ValidateNotNullOrEmpty()] 120 | [string] $EncryptedBase64String, 121 | 122 | [Parameter( 123 | ParameterSetName='store', 124 | Position=1, 125 | Mandatory=$true)] 126 | [ValidateNotNullOrEmpty()] 127 | [string] $CertThumbprint, 128 | 129 | [Parameter( 130 | ParameterSetName='store', 131 | Position=2, 132 | Mandatory=$false)] 133 | [ValidateNotNullOrEmpty()] 134 | [string] $CertStore = "cert:\LocalMachine\My", 135 | 136 | [Parameter( 137 | ParameterSetName='file', 138 | Position=1, 139 | Mandatory=$true)] 140 | [ValidateNotNullOrEmpty()] 141 | [string] $PrivateCertPath, 142 | 143 | [Parameter( 144 | ParameterSetName='file', 145 | Position=2, 146 | Mandatory=$true)] 147 | [ValidateNotNullOrEmpty()] 148 | [string] $PrivateCertPassword 149 | ) 150 | 151 | # Decrypts cipher text using the private key 152 | # Assumes the certificate is in the LocalMachine\My (Personal) Store 153 | switch ($PSCmdlet.ParameterSetName) 154 | { 155 | 'store' 156 | { 157 | $cert = Get-ChildItem -Path $CertStore | Where-Object { $_.Thumbprint -eq $CertThumbprint } 158 | } 159 | 160 | 'file' 161 | { 162 | $PrivateCertPath = Resolve-Path -Path $PrivateCertPath 163 | $privateCertSecurePassword = $PrivateCertPassword | ConvertTo-SecureString -AsPlainText -Force 164 | $cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList ($PrivateCertPath, $privateCertSecurePassword) 165 | } 166 | } 167 | 168 | if ($null -ne $cert) 169 | { 170 | $encryptedByteArray = [Convert]::FromBase64String($EncryptedBase64String) 171 | $clearText = [System.Text.Encoding]::UTF8.GetString( 172 | $cert.PrivateKey.Decrypt( 173 | $encryptedByteArray, 174 | $true)) 175 | } 176 | else 177 | { 178 | switch ($PSCmdlet.ParameterSetName) 179 | { 180 | 'store' 181 | { 182 | Write-Error "Certificate with thumbprint `"$CertThumbprint`" in store `"$CertStore`" not found." 183 | } 184 | 185 | 'file' 186 | { 187 | Write-Error "Certificate file `"$PrivateCertPath`" not found." 188 | } 189 | } 190 | 191 | } 192 | 193 | return $clearText 194 | } -------------------------------------------------------------------------------- /AzureService/RESTProxyContent/bin/StartupTasks/DomainJoined/Ensure-DomainJoined.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (C) Microsoft Corporation. All rights reserved. 2 | 3 | # Load the helper cmdlets 4 | . $PSScriptRoot\..\Helpers\Encryption.ps1 5 | . $PSScriptRoot\..\Helpers\GroupManagement.ps1 6 | 7 | function Get-JoinedDomain 8 | { 9 | <# 10 | .SYNOPSIS 11 | Gets the domain that the computer is currently joined to. 12 | 13 | .DESCRIPTION 14 | Gets the domain that the computer is currently joined to. 15 | 16 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 17 | 18 | .PARAMETER Computer 19 | The name of the computer to check the domain join status of. 20 | 21 | .EXAMPLE 22 | Get-JoinedDomain "MyComputerName" 23 | 24 | Returns back the computer name and its joined domain if the computer is joined 25 | to a computer, otherwise throws an exception. 26 | 27 | .OUTPUTS 28 | PSCustomObject - An object with a "Name" and "Domain" property for the computer and its 29 | domain. 30 | #> 31 | param ( 32 | [parameter( 33 | Position=0, 34 | ValueFromPipeline, 35 | ValueFromPipelineByPropertyName)] 36 | [string] $Computer = "." 37 | ) 38 | 39 | process 40 | { 41 | Get-CimInstance -Class Win32_ComputerSystem -ComputerName $Computer -ErrorAction Stop | 42 | Select-Object Name, Domain 43 | } 44 | } 45 | 46 | function Ensure-ComputerDomainJoined 47 | { 48 | <# 49 | .SYNOPSIS 50 | Ensures the computer is domain joined. If not, it will join the specified domain. 51 | 52 | .DESCRIPTION 53 | Ensures the computer is domain joined. If not, it will join the specified domain. 54 | 55 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 56 | 57 | .PARAMETER Domain 58 | The name of the domain that the current to check the domain join status of. 59 | 60 | .PARAMETER UserName 61 | The usename for the user that will be authenticating the request to join the domain. 62 | 63 | .PARAMETER Secret 64 | The Base64 encrypted password for UserName. 65 | 66 | .PARAMETER CertThumbprint 67 | The thumbprint (40-digit hex number) of the certificate to use to decode Secret. 68 | 69 | .PARAMETER CertStore 70 | The certificate store that the certificate with CertThumbrint can be found. 71 | 72 | .PARAMETER Computer 73 | The computer that should be joined to Domain. 74 | 75 | .EXAMPLE 76 | Ensure-ComputerDomainJoined -Domain "foo" -UserName "sampleUser" -Secret $encrypted -CertThumbrint 1234567890ABCDEF1234567890ABCDEF12345678 77 | 78 | Will ensure that the current computer is joined to the foo domain. If it's not, 79 | it will join the domain by authenticating with "sampleUser" and use the password that 80 | is found after decrypting $encrypted using the certificate found with the specified 81 | thumbprint in the local computer certificate store. 82 | #> 83 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 84 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "", Justification="We're getting the password in cleartext")] 85 | param( 86 | [Parameter(Mandatory)] 87 | [string] $Domain, 88 | 89 | [Parameter(Mandatory)] 90 | [string] $UserName, 91 | 92 | [Parameter(Mandatory)] 93 | [string] $Secret, 94 | 95 | [Parameter(Mandatory)] 96 | [string] $CertThumbprint, 97 | 98 | [string] $CertStore = "cert:\LocalMachine\My", 99 | 100 | [string] $Computer = "." 101 | ) 102 | 103 | process 104 | { 105 | Write-Verbose "Ensuring computer is joined the [$Domain] domain" 106 | 107 | try 108 | { 109 | $domainInfo = Get-JoinedDomain 110 | } 111 | catch 112 | { 113 | Write-Error "Unable get computer information." 114 | throw $_ 115 | } 116 | 117 | if ($domainInfo.Domain -ne $Domain) 118 | { 119 | Write-Verbose "Computer is not part of the specified domain. It will now be joined to the [$Domain] domain." 120 | 121 | try 122 | { 123 | Set-DnsClient -InterfaceAlias "Ethernet*" -ConnectionSpecificSuffix $Domain 124 | 125 | Write-Verbose "Decrypting password." 126 | $securePassword = Decrypt-Asymmetric -EncryptedBase64String $Secret -CertThumbprint $CertThumbprint -CertStore $CertStore -ErrorAction Stop | 127 | ConvertTo-SecureString -AsPlainText -Force -ErrorAction Stop 128 | 129 | Write-Verbose "Password decrypted." 130 | $cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList("$Domain\$UserName", $securePassword) 131 | 132 | # Specifically not using the -Restart switch because that causes problems with 133 | # the rest of the service deployment. Restarting doesn't actually appear to be 134 | # necessary anyway. In the event that we really did have to restart here, 135 | # we could always just do something like 'shutdown -r -t 90' to allow some 136 | # time for the service to finish deploying before the VM was restarted. 137 | Add-Computer -DomainName $Domain -Credential $cred -Force -ErrorAction Stop 138 | } 139 | catch 140 | { 141 | Write-Error "Unable to add computer to the [$Domain] domain." 142 | throw $_ 143 | } 144 | } 145 | else 146 | { 147 | Write-Verbose "Computer is already joined to the [$Domain] domain. Nothing to do." 148 | } 149 | } 150 | } 151 | 152 | function Ensure-DomainJoined 153 | { 154 | <# 155 | .SYNOPSIS 156 | Ensures the computer is domain joined. If not, it will join the specified domain. 157 | 158 | .DESCRIPTION 159 | Ensures the computer is domain joined. If not, it will join the specified domain. 160 | 161 | Additionally ensures that the specified user is an Administrator and a Remote Desktop user. 162 | 163 | The Git repo for this cmdlet can be found here: http://aka.ms/StoreBroker 164 | 165 | .PARAMETER Domain 166 | The name of the domain that the current to check the domain join status of. 167 | 168 | .PARAMETER UserName 169 | The usename for the user that will be authenticating the request to join the domain. 170 | 171 | .PARAMETER Secret 172 | The Base64 encrypted password for UserName. 173 | 174 | .PARAMETER CertThumbprint 175 | The thumbprint (40-digit hex number) of the certificate to use to decode Secret. 176 | 177 | .PARAMETER CertStore 178 | The certificate store that the certificate with CertThumbrint can be found. 179 | 180 | .PARAMETER Computer 181 | The computer that should be joined to Domain. 182 | 183 | .EXAMPLE 184 | Ensure-DomainJoined -Domain "foo" -UserName "sampleUser" -Secret $encrypted -CertThumbrint 1234567890ABCDEF1234567890ABCDEF12345678 185 | 186 | Will ensure that the current computer is joined to the foo domain. If it's not, 187 | it will join the domain by authenticating with "sampleUser" and use the password that 188 | is found after decrypting $encrypted using the certificate found with the specified 189 | thumbprint in the local computer certificate store. 190 | #> 191 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 192 | param( 193 | [Parameter(Mandatory)] 194 | [string] $Domain, 195 | 196 | [Parameter(Mandatory)] 197 | [string] $UserName, 198 | 199 | [Parameter(Mandatory)] 200 | [string] $Secret, 201 | 202 | [Parameter(Mandatory)] 203 | [string] $CertThumbprint, 204 | 205 | [string] $CertStore = "cert:\LocalMachine\My", 206 | 207 | [string] $Computer = "." 208 | ) 209 | 210 | Ensure-ComputerDomainJoined -Domain $Domain -UserName $UserName -Secret $Secret -CertThumbprint $CertThumbprint -CertStore $CertStore -Computer $Computer 211 | Ensure-IsInAdministratorsGroup -UserName $UserName -Domain $Domain 212 | Ensure-IsInRemoteDesktopUsersGroup -UserName $UserName -Domain $Domain 213 | } -------------------------------------------------------------------------------- /PDP/ProductDescription.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | My Localized AppName 11 | 12 | 13 | 14 | 15 | Video edit 16 | Captions 17 | Trim 18 | Soundtrack 19 | Video sharing 20 | 21 | 22 | 23 | 24 | Turn any video into a memorable moment you'll love to share. With this app, you can trim your video to your favorite parts, highlight key moments with captions and effects, and set the mood with music. 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Various performance optimiazations, this app is wicked-fast now. 55 | 56 | 57 | 58 | 59 | 60 | 66 | Video editing is easy 67 | 73 | Add matching music to your video 74 | 76 | Add text to your video 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | <!-- _locComment_text="{MaxLength=255} Trailer title 1" -->This is Trailer 1's title. 92 | 93 | Description of trailer 1's screenshot that no user will ever see. 94 | 95 | 96 | 97 | 98 | 99 | 100 | Pick your favorite 60 seconds. You can trim from the beginning, middle, or end. 101 | Highlight your favorite moments with fun, colorful captions in a range of styles. 102 | Add music that matches the moment. We've included a few songs for different moods, or you can use songs from your collection. 103 | When your movie is exactly how you want it, share it with family and friends right from the app. 104 | 105 | 106 | 107 | 108 | Computer 109 | Keyboard 110 | 111 | 112 | 113 | 114 | Zune music device 115 | Windows PC 116 | 117 | 118 | 119 | 120 | (c) Contoso LLC. 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | http://www.contoso.com/App 130 | 131 | 132 | 133 | 134 | support@contoso.com 135 | 136 | 137 | 138 | 139 | http://www.contoso.com/App/privacy 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /StoreBroker/IapConfigTemplate.json: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | 3 | // This configuration file is for use with the StoreBroker PowerShell module. 4 | // Configure details of the In-App Product ("add on") here, as well as the location of resources 5 | // used by the packaging tool. 6 | 7 | // Inline Comments can be placed in this file using the '//' delimiter. 8 | // The delimiter and any remaining text on that line will be removed. 9 | 10 | { 11 | // New-InAppProductSubmissionPackage parameters 12 | // 13 | // If you notice one of the parameters you specify to New-InAppProductSubmissionPackage does not change, you can specify it here 14 | // in order to avoid providing it at runtime. If New-InAppProductSubmissionPackage does not have a parameter it needs, it will 15 | // check the provided config file for a non-null, non-empty value. The exception to this is -ConfigPath which must 16 | // always be provided. For information on these parameters see the README.md or run `help New-InAppProductSubmissionPackage -ShowWindow` 17 | // 18 | // WARNING: Specifying a parameter at runtime will ignore any value placed in the config file. 19 | 20 | "packageParameters": { 21 | 22 | // There are two supported layouts for your PDP files: 23 | // 1. \\...\PDP.xml 24 | // 2. \\\...\PDP.xml 25 | // The only difference between these two is that there is a directory after the 26 | // and before the sub-directories. 27 | // 28 | // The first layout is generally used when your localization system will be downloading 29 | // the localized PDP files during your build. In that situation, it's always retrieving 30 | // the latest version. Alternatively, if the latest localized versions of your PDP 31 | // files are always stored in the same location, this layout is also for you. 32 | // 33 | // On the other hand, if you will be archiving the localized PDP's based on each release 34 | // to the Store, then the second layout is the one that you should use. In this scenario, 35 | // you will specify the value of "" immediately below, or at the commandline. 36 | // 37 | // Only specify 'PDPRootPath' here if it does not change. 38 | // DO NOT use environment variables in the config. If you need to use an environment 39 | // variable to get to the right path, then you need to specify that path at the commandline 40 | // instead. 41 | // 42 | // File separator '\' must be escaped, i.e. '\\' 43 | // Ex: \\\\filehare\\Public\\AppPDPs\\MyApp 44 | 45 | "PDPRootPath": "", 46 | 47 | // If your PDP files are being placed in a directory structure like this: 48 | // \\\...\PDP.xml 49 | // then specify the correct 'Release' here or at runtime. 50 | 51 | "Release": "", 52 | 53 | // Filenames that SHOULD be processed. 54 | // Wildcards are allowed, eg "InAppProductDescription*.xml". 55 | // It is fine to specify both "PDPInclude" and "PDPExclude". 56 | // 57 | // Ex: "PDPInclude": [ 58 | // "IapPDP*.xml", <- Comma to separate items 59 | // "InAppProductDescription.xml" <- No comma for last item, or JSON deserialization will fail 60 | // ] 61 | 62 | "PDPInclude": [ 63 | ], 64 | 65 | // Filenames that SHOULD NOT be processed. 66 | // Wildcards are allowed, eg "InAppProductDescription*.xml". 67 | // It is fine to specify both "PDPInclude" and "PDPExclude". 68 | // It is NOT necessary to specify something here, but you can to be more explicit. 69 | // 70 | // Ex: "PDPExclude": [ 71 | // "*.xml.lss", <- Comma to separate items 72 | // "*.xml.lct" <- No comma for last item, or JSON deserialization will fail 73 | // ] 74 | 75 | "PDPExclude": [ 76 | ], 77 | 78 | // Languages to be excluded from processing by New-InAppProductSubmissionPackage. 79 | // New-InAppProductSubmissionPackage will use the filepath of a PDP XML file 80 | // to identify the language of the metadata. If it sees a match 81 | // with a lang-code in this list, it will skip processing the file. 82 | // 83 | // Ex: "LanguageExclude": [ 84 | // "default", <- Comma to separate items 85 | // "qps-ploc" <- No comma for last item, or JSON deserialization will fail 86 | // ], 87 | 88 | "LanguageExclude": [ 89 | "default" 90 | ], 91 | 92 | // Your store screenshots must be placed with this structure: 93 | // \\\...\img.png 94 | // Specify 'ImagesRootPath' here. 95 | // 96 | // IMPORTANT: The 'Release' that will be used here is *NOT* the value specified to 97 | // New-InAppProductSubmissionPackage (nor is it the Release value specified earlier in this config file), 98 | // but rather the 'Release' attribute at the top of the corresponding PDP file. 99 | // 100 | // File separator '\' must be escaped, i.e. '\\' 101 | // Ex: \\\\fileshare\\Public\\Screenshots\\MyApp 102 | 103 | "ImagesRootPath": "", 104 | 105 | // Some apps may not localize all of their metadata media (images, trailers, etc..) across all languages. 106 | // By default, StoreBroker will look in the PDP langcode's subfolder within ImagesRootPath for that 107 | // language's media content. If the requested filename is not found, StoreBroker packaging will fail. 108 | // If you specify a fallback language here (e.g. 'en-us'), then if the requested file isn't found in 109 | // the PDP language's media subfolder, StoreBroker will then look into the fallback language's media 110 | // subfolder for the exactly same-named image before failing. 111 | 112 | "MediaFallbackLanguage": "", 113 | 114 | // Full path to a directory where the Packaging Tool can write the .json submission request 115 | // body and .zip package to upload. 116 | // 117 | // File separator '\' must be escaped, i.e. '\\' 118 | // Ex: "OutPath": "C:\\app\\output" 119 | 120 | "OutPath": "", 121 | 122 | // Common name to give to the .json and .zip files outputted by the Packaging Tool. 123 | 124 | "OutName": "" 125 | }, 126 | 127 | // Configure details of the IAP Submission 128 | 129 | "iapSubmission": { 130 | 131 | // If known, this is the IapId given to your in-app application ("add on") by the Windows Store. 132 | // This isn't required, but adding this will help protect you from submitting something 133 | // to the wrong product when working at the commandline. 134 | // 135 | // Ex: "iapId": "0ABCDEF12345" 136 | "iapId": "", 137 | 138 | ///////////////////////////////// 139 | // // 140 | // PUBLISH MODE AND VISIBILITY // 141 | // // 142 | ///////////////////////////////// 143 | 144 | // Publish mode of the submission. One of ["NotSet", "Immediate", "Manual", "SpecificDate"] 145 | 146 | "targetPublishMode": "NotSet", 147 | 148 | // Publish date-time of the submission if the "targetPublishMode" is specified as "SpecificDate". 149 | // Date-time format should follow the ISO 8601 standard. 150 | // Ex: "2000-01-30T00:00:00-08:00" 151 | 152 | "targetPublishDate": null, 153 | 154 | // Visibility of the add-on. One of ["NotSet", "Public", "Private", "Hidden"] 155 | // 156 | // Public: Anyone can find your add-on in the Store. 157 | // Private: Hide this add-on in the Store. Customers with a direct link to the add-on's listing can still download 158 | // it, except on Windows 8 and Windows 8.1. 159 | // Hidden: Hide this add-on and prevent acquisition. Customers with a promotional code can still download it on 160 | // Windows 10 devices. 161 | 162 | "visibility": "NotSet", 163 | 164 | /////////////////////// 165 | // // 166 | // PROPERTIES // 167 | // // 168 | /////////////////////// 169 | 170 | // lifetime 171 | // one of: 172 | // ["Forever", "OneDay", "ThreeDays", "FiveDays", "OneWeek", "TwoWeeks", "OneMonth", "TwoMonths", "ThreeMonths", "SixMonths", "OneYear"] 173 | 174 | "lifetime": "Forever", 175 | 176 | // contentType - Regardless of your add-on's product type, you'll also need to indicate the 177 | // type of content you're offering. For most add-ons, the content type should be "OnlineDownload". 178 | // If another option from the list seems to describe your add-on better (for example, if you 179 | // are offering a music download or an e-book), select that option instead. 180 | // 181 | // one of: 182 | // ["NotSet", "BookDownload", "EMagazine", "ENewspaper", "MusicDownload", "MusicStream", 183 | // "OnlineDataStorage", "VideoDownload", "VideoStream", "Asp", "OnlineDownload"] 184 | 185 | "contentType": "NotSet", 186 | 187 | // Your app can then query for add-ons that match these words. 188 | // This feature lets you build screens in your app that can load add-ons without you having to 189 | // directly specify the product ID in your app's code. You can then change the add-on's keywords 190 | // anytime, without having to make code changes in your app or submit the app again. 191 | // Maximum: 10 keywords of up to 30 characters each 192 | "keywords": [ 193 | ], 194 | 195 | // The custom developer data for the add-on (this information was previously called the 'tag'). 196 | // You can use this to provide extra context for your in-app product. 197 | // Your app can query this value dynamically, and you can adjust the value at any time by 198 | // updating the info in the add-on's Custom developer data field, without having to make code 199 | // changes in your app or submit the app again. 200 | // Maximum: 3000 characters 201 | // The contents must be provided on one line, but it is possible to specify multiline content using "\r\n". 202 | // Ex: "tag": "These\r\nare\r\nfour\r\nlines" 203 | "tag": "", 204 | 205 | ////////////////////////////// 206 | // // 207 | // PRICING AND AVAILABILITY // 208 | // // 209 | ////////////////////////////// 210 | 211 | // Pricing 212 | // 213 | // Your add-on will be priced according to the price tier set for "priceId" and will be marketed at 214 | // that tier for ALL available markets. To set a different price for a specific market, use the 215 | // "marketSpecificPricings" object. You can also use the "marketSpecificPricings" object to make 216 | // your add-on "NotAvailable" to a specific market. 217 | // 218 | // It is not currently possible to programmatically access or set sales information via the 219 | // Windows Store Submission API for add-on submissions at this time. 220 | 221 | "pricing": { 222 | 223 | // Price 224 | // 225 | // Set the price of the add-on. 226 | // The value provided should be a "Tier" and NOT an actual price 227 | // For a mapping of "Tiers" to prices, refer to https://docs.microsoft.com/en-us/windows/uwp/monetize/manage-add-on-submissions#price-tiers 228 | // Can also be "Free" or "NotAvailable" 229 | 230 | "priceId": "NotAvailable", 231 | 232 | // Market Specific Pricing 233 | // 234 | // By default, your add-on will be marketed at the price tier set for "priceId" for all possible markets. 235 | // To set a different price tier for a specific region, add the ("region": "Tier") key-value pair. 236 | // 237 | // For a list of supported regions, see https://msdn.microsoft.com/en-us/library/windows/apps/mt148563.aspx 238 | 239 | "marketSpecificPricings": { 240 | } 241 | }, 242 | 243 | ///////////////////////////// 244 | // // 245 | // NOTES FOR CERTIFICATION // 246 | // // 247 | ///////////////////////////// 248 | 249 | // String. Information that helps testers use and understand this submission. 250 | // Customers won't see this info. 251 | // Can include information such as test account credentials, steps to access and verify features, etc. 252 | // 253 | // The notes must be provided on one line, but it is possible to specify multiline notes using "\r\n". 254 | // Ex: "notesForCertification": "These\r\nare\r\nfour\r\nlines" 255 | 256 | "notesForCertification": "" 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /StoreBroker/NugetTools.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (C) Microsoft Corporation. All rights reserved. 2 | 3 | function Get-NugetExe 4 | { 5 | <# 6 | .SYNOPSIS 7 | Downloads nuget.exe from http://nuget.org to a new local temporary directory 8 | and returns the path to the local copy. 9 | 10 | .DESCRIPTION 11 | Downloads nuget.exe from http://nuget.org to a new local temporary directory 12 | and returns the path to the local copy. 13 | 14 | The Git repo for this module can be found here: http://aka.ms/StoreBroker 15 | 16 | .EXAMPLE 17 | Get-NugetExe 18 | Creates a new directory with a GUID under $env:TEMP and then downloads 19 | http://nuget.org/nuget.exe to that location. 20 | 21 | .OUTPUTS 22 | System.String - The path to the newly downloaded nuget.exe 23 | #> 24 | [CmdletBinding(SupportsShouldProcess)] 25 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] 26 | param() 27 | 28 | if ($null -eq $script:nugetExePath) 29 | { 30 | $sourceNugetExe = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" 31 | $script:nugetExePath = Join-Path $(New-TemporaryDirectory) "nuget.exe" 32 | 33 | Write-Log -Message "Downloading $sourceNugetExe to $script:nugetExePath" -Level Verbose 34 | Invoke-WebRequest $sourceNugetExe -OutFile $script:nugetExePath 35 | } 36 | 37 | return $script:nugetExePath 38 | } 39 | 40 | function Get-NugetPackage 41 | { 42 | <# 43 | .SYNOPSIS 44 | Downloads a nuget package to the specified directory. 45 | 46 | .DESCRIPTION 47 | Downloads a nuget package to the specified directory (or the current 48 | directory if no TargetPath was specified). 49 | 50 | The Git repo for this module can be found here: http://aka.ms/StoreBroker 51 | 52 | .PARAMETER PackageName 53 | The name of the nuget package to download 54 | 55 | .PARAMETER TargetPath 56 | The nuget package will be downloaded to this location. 57 | 58 | .PARAMETER Version 59 | If provided, this indicates the version of the package to download. 60 | If not specified, downloads the latest version. 61 | 62 | .PARAMETER NoStatus 63 | If this switch is specified, long-running commands will run on the main thread 64 | with no commandline status update. When not specified, those commands run in 65 | the background, enabling the command prompt to provide status information. 66 | 67 | .EXAMPLE 68 | Get-NugetPackage "Microsoft.AzureStorage" -Version "6.0.0.0" -TargetPath "c:\foo" 69 | Downloads v6.0.0.0 of the Microsoft.AzureStorage nuget package to the c:\foo directory. 70 | 71 | .EXAMPLE 72 | Get-NugetPackage "Microsoft.AzureStorage" "c:\foo" 73 | Downloads the most recent version of the Microsoft.AzureStorage 74 | nuget package to the c:\foo directory. 75 | #> 76 | [CmdletBinding(SupportsShouldProcess)] 77 | param( 78 | [Parameter( 79 | Mandatory, 80 | ValueFromPipeline)] 81 | [string] $PackageName, 82 | 83 | [Parameter(Mandatory)] 84 | [ValidateScript({if (Test-Path -Path $_ -PathType Container) { $true } else { throw "$_ does not exist." }})] 85 | [string] $TargetPath, 86 | 87 | [string] $Version = "", 88 | 89 | [switch] $NoStatus 90 | ) 91 | 92 | Write-Log -Message "Downloading nuget package [$PackageName] to [$TargetPath]" -Level Verbose 93 | 94 | $nugetPath = Get-NugetExe 95 | 96 | if ($NoStatus) 97 | { 98 | if ($PSCmdlet.ShouldProcess($PackageName, $nugetPath)) 99 | { 100 | if (-not [System.String]::IsNullOrEmpty($Version)) 101 | { 102 | & $nugetPath install $PackageName -o $TargetPath -version $Version -source nuget.org -NonInteractive | Out-Null 103 | } 104 | else 105 | { 106 | & $nugetPath install $PackageName -o $TargetPath -source nuget.org -NonInteractive | Out-Null 107 | } 108 | } 109 | } 110 | else 111 | { 112 | $jobName = "Get-NugetPackage-" + (Get-Date).ToFileTime().ToString() 113 | 114 | if ($PSCmdlet.ShouldProcess($jobName, "Start-Job")) 115 | { 116 | [scriptblock]$scriptBlock = { 117 | param($NugetPath, $PackageName, $TargetPath, $Version) 118 | 119 | if (-not [System.String]::IsNullOrEmpty($Version)) 120 | { 121 | & $NugetPath install $PackageName -o $TargetPath -version $Version -source nuget.org 122 | } 123 | else 124 | { 125 | & $NugetPath install $PackageName -o $TargetPath -source nuget.org 126 | } 127 | } 128 | 129 | Start-Job -Name $jobName -ScriptBlock $scriptBlock -Arg @($nugetPath, $PackageName, $TargetPath, $Version) | Out-Null 130 | 131 | if ($PSCmdlet.ShouldProcess($jobName, "Wait-JobWithAnimation")) 132 | { 133 | Wait-JobWithAnimation -JobName $jobName -Description "Retrieving nuget package: $PackageName" 134 | } 135 | 136 | if ($PSCmdlet.ShouldProcess($jobName, "Receive-Job")) 137 | { 138 | Receive-Job $jobName -AutoRemoveJob -Wait -ErrorAction SilentlyContinue -ErrorVariable remoteErrors | Out-Null 139 | } 140 | } 141 | 142 | if ($remoteErrors.Count -gt 0) 143 | { 144 | throw $remoteErrors[0].Exception 145 | } 146 | } 147 | } 148 | 149 | function Test-AssemblyIsDesiredVersion 150 | { 151 | <# 152 | .SYNOPSIS 153 | Checks if the specified file is the expected version. 154 | 155 | .DESCRIPTION 156 | Checks if the specified file is the expected version. 157 | 158 | Does a best effort match. If you only specify a desired version of "6", 159 | any version of the file that has a "major" version of 6 will be considered 160 | a match, where we use the terminology of a version being: 161 | Major.Minor.Build.PrivateInfo. 162 | 163 | The Git repo for this module can be found here: http://aka.ms/StoreBroker 164 | 165 | .PARAMETER AssemblyPath 166 | The full path to the assembly file being tested. 167 | 168 | .PARAMETER DesiredVersion 169 | The desired version of the assembly. Specify the version as specifically as 170 | necessary. 171 | 172 | .EXAMPLE 173 | Test-AssemblyIsDesiredVersion "c:\Microsoft.WindowsAzure.Storage.dll" "6" 174 | 175 | Returns back $true if "c:\Microsoft.WindowsAzure.Storage.dll" has a major version 176 | of 6, regardless of its Minor, Build or PrivateInfo numbers. 177 | 178 | .OUTPUTS 179 | Boolean - $true if the assembly at the specified path exists and meets the specified 180 | version criteria, $false otherwise. 181 | #> 182 | param( 183 | [Parameter(Mandatory)] 184 | [ValidateScript( { if (Test-Path -PathType Leaf -Path $_) { $true } else { throw "'$_' cannot be found." } })] 185 | [string] $AssemblyPath, 186 | 187 | [Parameter(Mandatory)] 188 | [ValidateScript( { if ($_ -match '^\d+(\.\d+){0,3}$') { $true } else { throw "'$_' not a valid version format." } })] 189 | [string] $DesiredVersion 190 | ) 191 | 192 | $splitTargetVer = $DesiredVersion.Split('.') 193 | 194 | $file = Get-Item -Path $AssemblyPath -ErrorVariable ev 195 | if (($null -ne $ev) -and ($ev.Count -gt 0)) 196 | { 197 | Write-Log "Problem accessing [$Path]: $($ev[0].Exception.Message)" -Level Warning 198 | return $false 199 | } 200 | 201 | $versionInfo = $file.VersionInfo 202 | $splitSourceVer = @( 203 | $versionInfo.ProductMajorPart, 204 | $versionInfo.ProductMinorPart, 205 | $versionInfo.ProductBuildPart, 206 | $versionInfo.ProductPrivatePart 207 | ) 208 | 209 | # The cmdlet contract states that we only care about matching 210 | # as much of the version number as the user has supplied. 211 | for ($i = 0; $i -lt $splitTargetVer.Count; $i++) 212 | { 213 | if ($splitSourceVer[$i] -ne $splitTargetVer[$i]) 214 | { 215 | return $false 216 | } 217 | } 218 | 219 | return $true 220 | } 221 | 222 | function Get-NugetPackageDllPath 223 | { 224 | <# 225 | .SYNOPSIS 226 | Makes sure that the specified assembly from a nuget package is available 227 | on the machine, and returns the path to it. 228 | 229 | .DESCRIPTION 230 | Makes sure that the specified assembly from a nuget package is available 231 | on the machine, and returns the path to it. 232 | 233 | This will first look for the assembly in the module's script directory. 234 | 235 | Next it will look for the assembly in the location defined by 236 | $SBAlternateAssemblyDir. This value would have to be defined by the user 237 | prior to execution of this cmdlet. 238 | 239 | If not found there, it will look in a temp folder established during this 240 | PowerShell session. 241 | 242 | If still not found, it will download the nuget package 243 | for it to a temp folder accessible during this PowerShell session. 244 | 245 | The Git repo for this module can be found here: http://aka.ms/StoreBroker 246 | 247 | .PARAMETER NugetPackageName 248 | The name of the nuget package to download 249 | 250 | .PARAMETER NugetPackageVersion 251 | Indicates the version of the package to download. 252 | 253 | .PARAMETER AssemblyPackageTailDirectory 254 | The sub-path within the nuget package download location where the assembly should be found. 255 | 256 | .PARAMETER AssemblyName 257 | The name of the actual assembly that the user is looking for. 258 | 259 | .PARAMETER NoStatus 260 | If this switch is specified, long-running commands will run on the main thread 261 | with no commandline status update. When not specified, those commands run in 262 | the background, enabling the command prompt to provide status information. 263 | 264 | .EXAMPLE 265 | Get-NugetPackageDllPath "WindowsAzure.Storage" "6.0.0" "WindowsAzure.Storage.6.0.0\lib\net40\" "Microsoft.WindowsAzure.Storage.dll" 266 | 267 | Returns back the path to "Microsoft.WindowsAzure.Storage.dll", which is part of the 268 | "WindowsAzure.Storage" nuget package. If the package has to be downloaded via nuget, 269 | the command prompt will show a time duration status counter while the package is being 270 | downloaded. 271 | 272 | .EXAMPLE 273 | Get-NugetPackageDllPath "WindowsAzure.Storage" "6.0.0" "WindowsAzure.Storage.6.0.0\lib\net40\" "Microsoft.WindowsAzure.Storage.dll" -NoStatus 274 | 275 | Returns back the path to "Microsoft.WindowsAzure.Storage.dll", which is part of the 276 | "WindowsAzure.Storage" nuget package. If the package has to be downloaded via nuget, 277 | the command prompt will appear to hang during this time. 278 | 279 | .OUTPUTS 280 | System.String - The full path to $AssemblyName. 281 | #> 282 | [CmdletBinding(SupportsShouldProcess)] 283 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Justification="Methods called within here make use of PSShouldProcess, and the switch is passed on to them inherently.")] 284 | param( 285 | [Parameter(Mandatory)] 286 | [string] $NugetPackageName, 287 | 288 | [Parameter(Mandatory)] 289 | [string] $NugetPackageVersion, 290 | 291 | [Parameter(Mandatory)] 292 | [string] $AssemblyPackageTailDirectory, 293 | 294 | [Parameter(Mandatory)] 295 | [string] $AssemblyName, 296 | 297 | [switch] $NoStatus 298 | ) 299 | 300 | Write-Log -Message "Looking for $AssemblyName" -Level Verbose 301 | 302 | # First we'll check to see if the user has cached the assembly into the module's script directory 303 | $moduleAssembly = Join-Path $PSScriptRoot $AssemblyName 304 | if (Test-Path -Path $moduleAssembly -PathType Leaf -ErrorAction Ignore) 305 | { 306 | if (Test-AssemblyIsDesiredVersion -AssemblyPath $moduleAssembly -DesiredVersion $NugetPackageVersion) 307 | { 308 | Write-Log -Message "Found $AssemblyName in module directory ($PSScriptRoot)." -Level Verbose 309 | return $moduleAssembly 310 | } 311 | else 312 | { 313 | Write-Log -Message "Found $AssemblyName in module directory ($PSScriptRoot), but its version number didn't match required [$NugetPackageVersion]." -Level Verbose 314 | } 315 | } 316 | 317 | # Next, we'll check to see if the user has defined an alternate path to get the assembly from 318 | if (-not [System.String]::IsNullOrEmpty($SBAlternateAssemblyDir)) 319 | { 320 | $alternateAssemblyPath = Join-Path $SBAlternateAssemblyDir $AssemblyName 321 | if (Test-Path -Path $alternateAssemblyPath -PathType Leaf -ErrorAction Ignore) 322 | { 323 | if (Test-AssemblyIsDesiredVersion -AssemblyPath $alternateAssemblyPath -DesiredVersion $NugetPackageVersion) 324 | { 325 | Write-Log -Message "Found $AssemblyName in alternate directory ($SBAlternateAssemblyDir)." -Level Verbose 326 | return $alternateAssemblyPath 327 | } 328 | else 329 | { 330 | Write-Log -Message "Found $AssemblyName in alternate directory ($SBAlternateAssemblyDir), but its version number didn't match required [$NugetPackageVersion]." -Level Verbose 331 | } 332 | } 333 | } 334 | 335 | # Then we'll check to see if we've previously cached the assembly in a temp folder during this PowerShell session 336 | if ([System.String]::IsNullOrEmpty($script:tempAssemblyCacheDir)) 337 | { 338 | $script:tempAssemblyCacheDir = New-TemporaryDirectory 339 | } 340 | else 341 | { 342 | $cachedAssemblyPath = Join-Path $(Join-Path $script:tempAssemblyCacheDir $AssemblyPackageTailDirectory) $AssemblyName 343 | if (Test-Path -Path $cachedAssemblyPath -PathType Leaf -ErrorAction Ignore) 344 | { 345 | if (Test-AssemblyIsDesiredVersion -AssemblyPath $cachedAssemblyPath -DesiredVersion $NugetPackageVersion) 346 | { 347 | Write-Log -Message "Found $AssemblyName in temp directory ($script:tempAssemblyCacheDir)." -Level Verbose 348 | return $cachedAssemblyPath 349 | } 350 | else 351 | { 352 | Write-Log -Message "Found $AssemblyName in temp directory ($script:tempAssemblyCacheDir), but its version number didn't match required [$NugetPackageVersion]." -Level Verbose 353 | } 354 | } 355 | } 356 | 357 | # Still not found, so we'll go ahead and download the package via nuget. 358 | Write-Log -Message "$AssemblyName is needed and wasn't found. Acquiring it via nuget..." -Level Verbose 359 | Get-NugetPackage -PackageName $NugetPackageName -Version $NugetPackageVersion -TargetPath $script:tempAssemblyCacheDir -NoStatus:$NoStatus 360 | 361 | $cachedAssemblyPath = Join-Path $(Join-Path $script:tempAssemblyCacheDir $AssemblyPackageTailDirectory) $AssemblyName 362 | if (Test-Path -Path $cachedAssemblyPath -PathType Leaf -ErrorAction Ignore) 363 | { 364 | Write-Log -Message @( 365 | "To avoid this download delay in the future, copy the following file:", 366 | " [$cachedAssemblyPath]", 367 | "either to:", 368 | " [$PSScriptRoot]", 369 | "or to:", 370 | " a directory of your choosing, and save that directory path to `$SBAlternateAssemblyDir") 371 | 372 | return $cachedAssemblyPath 373 | } 374 | 375 | $output = "Unable to acquire a reference to $AssemblyName." 376 | Write-Log -Message $output -Level Error 377 | throw $output 378 | } 379 | -------------------------------------------------------------------------------- /Documentation/PDP.md: -------------------------------------------------------------------------------- 1 | # StoreBroker PowerShell Module 2 | ## PDP (Product Description Page) Files 3 | 4 | 5 | ---------- 6 | #### Table of Contents 7 | 8 | * [Overview](#overview) 9 | * [Sections](#sections) 10 | * [AppStoreName](#appstorename) 11 | * [Screenshots and Captions](#screenshots-and-captions) 12 | * [Folder Structure](#folder-structure) 13 | * [Additional Assets](#additional-assets) 14 | * [Trailers](#trailers) 15 | * [Icons](#icons) 16 | * [Fallback Language Support](#fallback-language-support) 17 | * [Schemas And Samples](#schemas-and-samples) 18 | * [Loc Attributes and Comments](#loc-attributes-and-comments) 19 | * [Marking A String To Not Be Localized](#marking-a-string-to-not-be-localized) 20 | 21 | ---------- 22 | 23 | 24 | ## Overview 25 | 26 | One of the biggest benefits of using StoreBroker and the Windows Store Submission API 27 | to update your submission, is the ease with which they can update all of the listing 28 | metadata (descriptions, features, screenshots and captions, etc...) across every language 29 | that you have localized metadata for. 30 | 31 | StoreBroker uses an XML file format that we refer to as "PDP" (Product Description Page) 32 | to store all of this metadata, making it easy for localization systems to parse and 33 | localize the relevant content. 34 | 35 | > There is no requirement that you adopt the PDP XML file format that we are discussing here. 36 | > `New-SubmissionPackage` generates the .json and .zip files ("the payload") that the other 37 | > commands interact with, and it is the only part of StoreBroker that has knowledge of PDP 38 | > files. If you are already using some other file format for the localization of this 39 | > metadata content, but you want to use StoreBroker, then you will have to write the necessary 40 | > code to generate the payload. Refer to the 41 | > [Submission API Documentation](https://msdn.microsoft.com/en-us/windows/uwp/monetize/manage-app-submissions) 42 | > for more information on this. 43 | 44 | ---------- 45 | 46 | ## Sections 47 | 48 | The main sections that you'll find in the PDP (depending on which schema version is in use) 49 | are as follows: 50 | 51 | **For Application Submissions** 52 | * AppStoreName 53 | * Keywords 54 | * Description 55 | * ShortDescription 56 | * ShortTitle 57 | * SortTitle 58 | * VoiceTitle 59 | * DevStudio 60 | * ReleaseNotes 61 | * ScreenshotCaptions 62 | * AdditionalAssets 63 | * AppFeatures 64 | * RecommendedHardware 65 | * MinimumHardware 66 | * CopyrightAndTrademark 67 | * AdditionalLicenseTerms 68 | * WebsiteURL 69 | * SupportContactInfo 70 | * PrivacyPolicyURL 71 | * Trailers 72 | 73 | **For In-App Product (IAP) ("add-on") Submissions** 74 | * Title 75 | * Description 76 | * Icon 77 | 78 | These should map fairly clearly to the sections you're already familiar with in the DevPortal. 79 | For additional requirements on number of elements or length of individual elements, refer to 80 | the schema (or sample file). 81 | 82 | ### AppStoreName 83 | 84 | > The `AppStoreName` property in the PDP file is often a source of confusion for users, so it's recommended 85 | > that you pay close attention this expanded explanation below. 86 | 87 | `AppStoreName` maps to the 88 | [`DisplayName`](https://docs.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/element-displayname) 89 | property from your application's AppxManifest.xml file. The Store will automatically grab this for every language 90 | that your application has a listing for, meaining that in most scenarios, you never need to provide a value within 91 | the PDP. The only time you should ever specify a value for this in the PDP, is if you will be providing a listing 92 | for a language that your application isn't explicitly being localized to. 93 | 94 | If you provide this value in a PDP for a language that your application already has a `DisplayName` value for, 95 | your submission will eventually fail after it has been committed because of this name conflict. 96 | 97 | **Re-stated again**, you can _only_ successfully provide the `AppStoreName` if _that PDP language_ does not have 98 | a corresponding `DisplayName` entry within your application package. 99 | 100 | If you're in the scenario where you _do_ need to do this for _some_ languages, you'll likely want to follow the 101 | [FAQ](./USAGE.md#faq) ("Does StoreBroker support adding region-specific listings for languages that the app itself 102 | doesn't directly support?") which explains how to use mulitple PDP's within your project, each localized to a 103 | different set of languages. In that scenario, you'd have a PDP with the `AppStoreName` uncommented and localized, 104 | but only for the set of languages that needs it; a different PDP would be used for the other languages, and in that 105 | other PDP, the `AppStoreName` would remain empty/commented-out. 106 | 107 | ### Screenshots and Captions 108 | 109 | > Only relevant for Application submissions 110 | 111 | It's worth a little bit of additional time to explain how screenshots and captions work. 112 | 113 | The DevPortal has different screenshot sections per platform, and you need to specify a caption 114 | for each image that you upload. That makes sense from the DevPortal perspective where it wants 115 | to be as declarative and flexible as possible for you to author how you want your content displayed. 116 | 117 | That approach doesn't make as much sense from a localization perspective, because you likely 118 | use a similar set of screenshots across all platforms, and those screenshots would share the 119 | same localized caption. 120 | 121 | Since the PDP file is all about localizing text, it puts the _captions_ first, and you then associate 122 | screenshots to those captions by filename. There are five different platforms currently supported 123 | for screenshots: 124 | 125 | * DesktopImage 126 | * MobileImage 127 | * XboxImage 128 | * SurfaceHubImage 129 | * HoloLensImage 130 | 131 | You need to specify one or more platform image attributes for each caption (otherwise, there's no 132 | reason to bother localizing the caption). The value of the attribute is the name of the screenshot 133 | that will be found in [screenshot folder structure](#folder-structure). 134 | 135 | #### Folder Structure 136 | 137 | A key attribute to be aware of is the `Release` attribute that is part of the primary 138 | `ProductDescription` node. The value for `Release` is directly used by `New-SubmissionPackage` 139 | to find the screenshots referenced by the PDP. 140 | 141 | The expected folder structure layout for screenshots is as follows: 142 | 143 | \\\...\img.png 144 | 145 | where: 146 | * `ImagesRootPath`: specified in your config file or at the commandline 147 | * `Release`: this is the attribute being discussed 148 | * `lang-code`: the langCode for the language matching that of the PDP 149 | * `...`: any number of sub-folders ... we don't care about these...at this point, we're just 150 | looking recusively for the specific filename 151 | * `img.png`: the filename that you specified in the caption's platform-specific image attribute 152 | 153 | ### Additional Assets 154 | 155 | > Only relevant for Application submissions 156 | 157 | There are three different types of images that can be used for a listing: 158 | 1. Screenshots (these have captions) 159 | 2. Additional Assets (these are images that have no concept of captions) 160 | 3. Trailer screenshots (which also don't have captions, but are tied to trailers) 161 | 162 | In the [previous section](#screenshots-and-captions), we talked about screenshots 163 | (and their captions). Now we will talk about #2 (Additional Assets). 164 | 165 | You can learn more about the specifics of these different images and how they're used by referring to the 166 | [dev portal's online documentation](https://docs.microsoft.com/en-us/windows/uwp/publish/app-screenshots-and-images), 167 | and the related [API documentation](https://docs.microsoft.com/en-us/windows/uwp/monetize/manage-app-submissions#image-object). 168 | 169 | To define these assets, there is a top-level element called `AdditionalAssets` (which 170 | is a sibling to `ScreenshotCaptions`). It can contain any (or all) of the following 171 | elements: 172 | 173 | * `StoreLogo9x16` 174 | * `StoreLogoSquare` 175 | * `Icon` 176 | * `PromotionalArt16x9` 177 | * `PromotionalArtwork2400X1200` 178 | * `XboxBrandedKeyArt` 179 | * `XboxTitledHeroArt` 180 | * `XboxFeaturedPromotionalArt` 181 | * `SquareIcon358X358` 182 | * `BackgroundImage1000X800` 183 | * `PromotionalArtwork414X180` 184 | 185 | These elements do not have any InnerText/content -- they only have a single attribute 186 | called `FileName` which should reference the .png file for that image type. 187 | 188 | Similar to Screenshots, there is full [fallback language support](#fallback-language-support). 189 | You can add the `FallbackLanguage` attribute on an individual element to only affect that one 190 | image type, or you can add it to `AdditionalAssets` to affect them all (or to 191 | `ProductDescription` to affect all asset types). 192 | 193 | #### Trailers 194 | 195 | > Only relevant for Application submissions 196 | 197 | You can learn more about the specifics of trailers and how they're used by referring to the 198 | [dev portal's online documentation](https://docs.microsoft.com/en-us/windows/uwp/publish/app-screenshots-and-images#trailers). 199 | 200 | A single trailer consists of the following information: 201 | * trailer filename 202 | * trailer title (localizable) 203 | * trailer screenshot filename (only one permitted) 204 | * trailer screenshot description (metadata only, never seen be a user) 205 | 206 | From an authoring perspective, it looks like this (with loc comment/attributes removed for brevity): 207 | 208 | 209 | 210 | This is the trailer's title 211 | 212 | The user will never see this text 213 | 214 | 215 | 216 | 217 | While companies may or may not provide localized screenshots per region, most companies will only 218 | ever produce their trailers for a limited number of different languages/regions, and re-use 219 | those same trailers across most other locales. Therefore, it is highly advised that you leverage 220 | [fallback language support](#fallback-language-support) when authoring your trailers 221 | so that trailers are propery re-used, thus reducing the size of the final package that needs 222 | to be uploaded to the Store. 223 | 224 | ### Icons 225 | 226 | > Only relevant for In-App Product (IAP) ("add-on") submissions 227 | 228 | Unlike screenshots, icons have no associated captions with them. However, it is possible that 229 | your IAP may need to use a different icon based on the region (maybe a different symbol or color 230 | is needed based on that region's culture), and so the icon is part of the PDP. 231 | 232 | The only thing that can be specified for the Icon is the filename of that icon. It is expected 233 | that the filename will be found within the defined [folder structure](#folder-structure). 234 | 235 | ### Fallback Language Support 236 | 237 | PDP files are language-specific, and as we saw in the [Folder Structure](#folder-structure) section, 238 | any screenhots or icons that are referenced by a PDP are only searched for within that same language's 239 | media sub-folder within `ImagesRootPath`. 240 | 241 | There are situations however where you might want to share an image/media file across more than one language. 242 | For instance: maybe you want all Spanish language PDP's to use the images from `es-es`. 243 | Both `New-SubmissionPackage` and `New-InAppProductSubmissionPackage` support a `MediaFallbackLanguage` parameter 244 | which lets you specify the language where StoreBroker should look if any of the referenced media files cannot 245 | be found in the language-specific images/media folder. 246 | 247 | For screenshot and icons, you can specify a `FallbackLanguage` attribute whose value would be a lang-code 248 | (ex. `en-US`, `es-es`, etc...). For icons, the attribute is directly on the `` element. For 249 | screenshots, the attribute is available on both the `` _and_ `` elements. 250 | It can be set on either, or both, of those elements. If specified on both, the value in the `` 251 | node value will win, since it is the more-specific value. 252 | 253 | Similarly, there is support for `AdditionalAsset` images (on the individual element nodes as well as on 254 | the `AdditionalAsset` node itself), and for trailers on the `Trailers`, `Trailer`, `Images` and `Image` 255 | elements). 256 | 257 | You can also set `FallbackLanguage` at the root element (`ProductDescription` or `InAppProductDescription`) 258 | to affect every media type. 259 | 260 | As usual, StoreBroker will first search for any media files referenced by that element in that PDP's 261 | langcode-specific images/media sub-folder. If not found, it will then look in the fallback language's 262 | images/media sub-folder. Only then will StoreBroker fail if the file still cannot be found. 263 | 264 | The key to remember is that this behaves in a "fallback" fashion, similar to language localization, and not 265 | as an override. StoreBroker will always attempt to use the language-specific version of the file unless 266 | it can't be found. 267 | 268 | > Specifying the `FallbackLanguage` attribute will override the `MediaFallbackLanguage` parameter/config value 269 | > for `New-SubmissionPackage` and `New-InAppProductSubmissionPackage` for that specific media element. 270 | > There can only be _one_ fallback language for any given media file. So, at most, StoreBroker will search 271 | > for a given media file twice (original language and fallback language) before failing the packaging action. 272 | 273 | ---------- 274 | 275 | ## Schemas and Samples 276 | 277 | At this time, StoreBroker has two PDP schemas in use: 278 | 279 | ### Application Submissions 280 | * **Uri**: `http://schemas.microsoft.com/appx/2012/ProductDescription` 281 | * **XSD**: [PDP\ProductDescription.xsd](../PDP/ProductDescription.xsd) 282 | * **Sample XML**: [PDP\ProductDescription.xml](../PDP/ProductDescription.xml) 283 | 284 | ### In-App Product (IAP) ("add-on") Submissions 285 | * **Uri**: `http://schemas.microsoft.com/appx/2012/InAppProductDescription` 286 | * **XSD**: [PDP\InAppProductDescription.xsd](../PDP/InAppProductDescription.xsd) 287 | * **Sample XML**: [PDP\InAppProductDescription.xml](../PDP/InAppProductDescription.xml) 288 | 289 | ---------- 290 | 291 | ## Loc Attributes and Comments 292 | 293 | If you reviewed the schema or the sample, you may have noticed some attributes named `_locID` 294 | or comments with the string `_locComment_text` in them. Those are there to assist with 295 | localization systems that don't send the raw file to localizers, but rather send a parsed 296 | representation of the file. 297 | 298 | The `_locID_` attribute is designed to give a unique ID (within the file) that localization 299 | systems can use to refer to the string. The `_locComment_text` comment is designed to be 300 | shown next to the localization string to give context to the localizers on how the string is 301 | being used, and can contain special instructions in brackets that give explicit guidance on 302 | requirements for the way that a string must be localized (ex: `{MaxLength=100}` would indicate 303 | that the final localized string can't exceed 100 characters). 304 | 305 | ### Marking A String To Not Be Localized 306 | 307 | In some situations, you may not want a certain string to be localized (ex: an email address or URL); 308 | this might be obvious to a human localizer, but likely not to an automated localization system. 309 | A common scenario where this might be an issue is if you use automated localization for 310 | [pseudolocalization](https://en.wikipedia.org/wiki/Pseudolocalization) purposes. 311 | 312 | The recommended way to indicate this in your PDP file is to modify that element's `_locComment_text`. 313 | Just change the part in the brackets (`{MaxLength=xxx}`) to instead say `{Locked}`. So, if it 314 | originally looked like this: 315 | 316 | 317 | 318 | change it to look like this: 319 | 320 | 321 | 322 | And then modify your localization system to recognize the comment for that element to indicate that 323 | it should not be localized. You should additionally publish this information to your localizers 324 | so that it can be quite explicit to them as well when they should/shouldn't localize a certain 325 | string. 326 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # StoreBroker PowerShell Module 2 | ## Contributing 3 | 4 | Looking to help out? You've come to the right place. We'd love your help in making this the best 5 | submission solution for every Windows developer. 6 | 7 | Looking for information on how to use this module? Head on over to [README.md](README.md). 8 | 9 | ---------- 10 | #### Table of Contents 11 | 12 | * [Overview](#overview) 13 | * [Maintainers](#maintainers) 14 | * [Feedback](#feedback) 15 | * [Bugs](#bugs) 16 | * [Suggestions](#suggestions) 17 | * [Questions](#questions) 18 | * [Static Analysis](#static-analysis) 19 | * [Visual Studio](#visual-studio) 20 | * [Module Manifest](#module-manifest) 21 | * [Logging](#logging) 22 | * [PowerShell Version](#powershell-version) 23 | * [Coding Guidelines](#coding-guidelines) 24 | * [Code Comments](#code-comments) 25 | * [Testing](#testing) 26 | * [Releasing](#releasing) 27 | * [Updating the CHANGELOG](#updating-the-changelog) 28 | * [Adding a New Tag](#adding-a-new-tag) 29 | * [Contributors](#contributors) 30 | * [Legal and Licensing](#legal-and-licensing) 31 | 32 | ---------- 33 | 34 | ## Overview 35 | 36 | We're excited that _you're_ excited about this project, and would welcome your contributions to help 37 | it grow. There are many different ways that you can contribute: 38 | 39 | 1. Submit a [bug report](#bugs). 40 | 2. Verify existing fixes for bugs. 41 | 3. Submit your own fixes for a bug. Before submitting, please make sure you have: 42 | * Performed code reviews of your own 43 | * Updated the [test cases](#testing) if needed 44 | * Run the [test cases](#testing) to ensure no feature breaks or test breaks 45 | * Added the [test cases](#testing) for new code 46 | * Ensured that the code is free of [static analysis](#static-analysis) issues 47 | 4. Submit a feature request. 48 | 5. Help answer [questions](https://github.com/Microsoft/StoreBroker/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Aquestion). 49 | 6. Write new [test cases](#testing). 50 | 7. Tell others about the project. 51 | 8. Tell the developers how much you appreciate the product! 52 | 53 | You might also read these two blog posts about contributing code: 54 | * [Open Source Contribution Etiquette](http://tirania.org/blog/archive/2010/Dec-31.html) by Miguel de Icaza 55 | * [Don't "Push" Your Pull Requests](http://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) by Ilya Grigorik. 56 | 57 | Before submitting a feature or substantial code contribution, please discuss it with the 58 | StoreBroker team via [Issues](https://github.com/Microsoft/StoreBroker/issues), and ensure it 59 | follows the product roadmap. Note that all code submissions will be rigorously reviewed by the 60 | StoreBroker Team. Only those that meet a high bar for both quality and roadmap fit will be merged 61 | into the source. 62 | 63 | ## Maintainers 64 | 65 | StoreBroker is maintained by: 66 | 67 | - **[@HowardWolosky](http://github.com/HowardWolosky)** 68 | - **[@DanBelcher-MSFT](http://github.com/DanBelcher-MSFT)** 69 | 70 | As StoreBroker is a production dependency for Microsoft, we have a couple workflow restrictions: 71 | 72 | - Anyone with commit rights can merge Pull Requests provided that there is a :+1: from one of 73 | the members above. 74 | - Releases are performed by a member above so that we can ensure Microsoft internal processes 75 | remain up to date with the latest and that there are no regressions. 76 | 77 | ## Feedback 78 | 79 | All issue types are tracked on the project's [Issues]( https://github.com/Microsoft/StoreBroker/issues) 80 | page. 81 | 82 | In all cases, make sure to search the list of issues before opening a new one. 83 | Duplicate issues will be closed. 84 | 85 | ### Bugs 86 | 87 | For a great primer on how to submit a great bug report, we recommend that you read: 88 | [Painless Bug Tracking](http://www.joelonsoftware.com/articles/fog0000000029.html). 89 | 90 | To report a bug, please include as much information as possible, namely: 91 | 92 | * The version of the module (located in `StoreBroker\StoreBroker.psd1`) 93 | * Your OS version 94 | * Your version of PowerShell (`$PSVersionTable.PSVersion`) 95 | * As much information as possible to reproduce the problem. 96 | * If possible, logs from your execution of the task that exhibit the erroneous behavior 97 | * The behavior you expect to see 98 | 99 | Please also mark your issue with the 'bug' label. 100 | 101 | ### Suggestions 102 | 103 | We welcome your suggestions for enhancements to the extension. 104 | To ensure that we can integrate your suggestions effectively, try to be as detailed as possible 105 | and include: 106 | 107 | * What you want to achieve / what is the problem that you want to address. 108 | * What is your approach for solving the problem. 109 | * If applicable, a user scenario of the feature / enhancement in action. 110 | 111 | Please also mark your issue with the 'suggestion' label. 112 | 113 | ### Questions 114 | 115 | If you've read through all of the documentation, checked the Wiki, and the PowerShell help for 116 | the command you're using still isn't enough, then please open an issue with the `question` 117 | label and include: 118 | 119 | * What you want to achieve / what is the problem that you want to address. 120 | * What have you tried so far. 121 | 122 | ---------- 123 | 124 | ## Static Analysis 125 | 126 | This project leverages the [PSScriptAnalyzer](https://github.com/PowerShell/PSScriptAnalyzer/) 127 | PowerShell module for static analysis. 128 | 129 | It is expected that this module shall remain "clean" from the perspective of that module. 130 | 131 | To run the module, from the root of your enlistment simply call 132 | 133 | Invoke-ScriptAnalyzer -Path .\ -Recurse 134 | 135 | That should return with no output. If you see any output when calling that command, 136 | either fix the issues that it calls out, or add a `[Diagnostics.CodeAnalysis.SuppressMessageAttribute()]` 137 | with a justification explaining why it's ok to suppress that rule within that part of the script. 138 | Refer to the [PSScriptAnalyzer documentation](https://github.com/PowerShell/PSScriptAnalyzer/) for 139 | more information on how to use that attribute, or look at other existing examples within this module. 140 | 141 | > Please ensure that your installation of PSScriptAnalyzer is up-to-date by running: 142 | > `Update-Module -Name PSScriptAnalyzer` 143 | > You should close and re-open your console window if the module was updated as a result of running 144 | > that command. 145 | 146 | ---------- 147 | 148 | ### Visual Studio 149 | 150 | A Visual Studio project exists for this module: 151 | 152 | StoreBroker.pssproj 153 | 154 | Even if **you** don't use Visual Studio to edit this module, others do, so if you 155 | add new files to the module, be sure that the VS project is updated as well. 156 | 157 | > To open this project in Visual Studio, you need to have the free 158 | > [PowerShell Tools For Visual Studio](https://visualstudiogallery.msdn.microsoft.com/c9eb3ba8-0c59-4944-9a62-6eee37294597) 159 | > extension installed. 160 | 161 | ---------- 162 | 163 | ### Module Manifest 164 | 165 | This is a manifested PowerShell module, and the manifest can be found here: 166 | 167 | StoreBroker\StoreBroker.psd1 168 | 169 | If you add any new modules/files to this module, be sure to update the manifest as well. 170 | New modules should be added to `NestedModules`, and any new functions or aliases that 171 | should be exported need to be added to the corresponding `FunctionsToExport` or 172 | `AliasesToExport` section. 173 | 174 | ---------- 175 | 176 | ### Logging 177 | 178 | Instead of using the built-in `Write-*` methods (`Write-Host`, `Write-Warning`, etc...), 179 | please use 180 | 181 | Write-Log 182 | 183 | which is implemented in Helpers.ps1. It will take care of formatting your content in a 184 | consistent manner, as well ensure that the content is logged to a file (if configured to do so 185 | by the user). 186 | 187 | ---------- 188 | 189 | ### PowerShell Version 190 | 191 | This module must be able to run on PowerShell version 4. It is permitted to add functionality 192 | that requires a higher version of PowerShell, but only if there is a fallback implementation 193 | that accomplishes the same thing in a PowerShell version 4 compatible way, and the path choice 194 | is controlled by a PowerShell version check. 195 | 196 | For an example of this, see `Write-Log` in `Helpers.ps1` which uses `Write-Information` 197 | for `Informational` messages on v5+ and falls back to `Write-Host` for earlier versions: 198 | 199 | if ($PSVersionTable.PSVersion.Major -ge 5) 200 | { 201 | Write-Information $ConsoleMessage -InformationAction Continue 202 | } 203 | else 204 | { 205 | Write-Host $ConsoleMessage 206 | } 207 | 208 | 209 | ---------- 210 | 211 | ### Coding Guidelines 212 | 213 | As a general rule, our coding convention is to follow the style of the surrounding code. 214 | Avoid reformatting any code when submitting a PR as it obscures the functional changes of your change. 215 | 216 | A basic rule of formatting is to use "Visual Studio defaults". 217 | Here are some general guidelines 218 | 219 | * No tabs, indent 4 spaces. 220 | * Braces usually go on their own line, 221 | with the exception of single line statements that are properly indented. 222 | * Use `camelCase` for instance fields, `PascalCase` for function and parameter names 223 | * Avoid the creation of `script` or `global` scoped variables unless absolutely necessary. 224 | If referencing a `script` or `global` scope variable, be sure to explicitly reference it by scope. 225 | * Avoid more than one blank empty line. 226 | * Always use a blank line following a closing bracket `}` unless the next line itself is a closing bracket. 227 | * Add full [Comment Based Help](https://technet.microsoft.com/en-us/library/hh847834.aspx) for all 228 | methods added, whether internal-only or external. The act of writing this documentation may help 229 | you better design your function. 230 | * File encoding should be ASCII (preferred) or UTF8 (with BOM) if absolutely necessary. 231 | * We try to adhere to the [PoshCode Best Practices](https://github.com/PoshCode/PowerShellPracticeAndStyle/tree/master/Best%20Practices) 232 | and [DSCResources Style Guidelines](https://github.com/PowerShell/DscResources/blob/master/StyleGuidelines.md) 233 | and think that you should too. 234 | * We try to limit lines to 100 characters to limit the amount of horizontal scrolling needed when 235 | reviewing/maintaining code. There are of course exceptions, but this is generally an enforced 236 | preference. The [Visual Studio Productivity Power Tools](https://visualstudiogallery.msdn.microsoft.com/34ebc6a2-2777-421d-8914-e29c1dfa7f5d) 237 | extension has a "Column Guides" feature that makes it easy to add a Guideline in column 100 238 | to make it really obvious when coding. 239 | 240 | ---------- 241 | 242 | ### Code comments 243 | 244 | It's strongly encouraged to add comments when you are making changes to the code and tests, 245 | especially when the changes are not trivial or may raise confusion. 246 | Make sure the added comments are accurate and easy to understand. 247 | Good code comments should improve readability of the code, and make it much more maintainable. 248 | 249 | That being said, some of the best code you can write is self-commenting. By refactoring your code 250 | into small, well-named functions that concisely describe their purpose, it's possible to write 251 | code that reads clearly while requiring minimal comments to understand what it's doing. 252 | 253 | ---------- 254 | 255 | ### Testing 256 | 257 | This module supports testing using the Pester UT framework. 258 | 259 | If you do not have Pester, download it [here](https://github.com/pester/Pester). 260 | Create a `Pester` folder under any path in `$env:PSModulePath`. 261 | Unzip the contents of the download to the `Pester` folder. 262 | Pester should now automatically import whenever you run a function from its module. 263 | 264 | In the StoreBroker module, the source tree and test tree are children of the project root path. 265 | Working code should be placed under `$root\StoreBroker`. Tests should be placed under `$root\Tests`. 266 | Each file in the `Tests` folder should test one file in the `StoreBroker` folder. For example, 267 | the file `$root\StoreBroker\PackageTool.ps1` should have a corresponding file 268 | `$root\Tests\PackageTool.Tests.ps1`. As the example shows, the filename of the test file 269 | indicates which file from the source tree it is testing. 270 | 271 | Tests can be run either from the project root directory or from the `Tests` subfolder. 272 | Navigate to the correct folder and simply run `Invoke-Pester`. 273 | 274 | Pester can also be used to test code-coverage, like so: 275 | 276 | Invoke-Pester -CodeCoverage "$root\StoreBroker\PackageTool.ps1" -TestName "*PackageTool*" 277 | 278 | This command tells Pester to check the `PackageTool.ps1` file for code-coverage. 279 | The `-TestName` parameter tells Pester to run any `Describe` blocks with a `Name` like 280 | `"*PackageTool*"`. 281 | 282 | The code-coverage object can be captured and interacted with, like so: 283 | 284 | $cc = (Invoke-Pester -CodeCoverage "$root\StoreBroker\PackageTool.ps1" -TestName "*PackageTool*" -PassThru -Quiet).CodeCoverage 285 | 286 | There are many more nuances to code-coverage, see 287 | [its documentation](https://github.com/pester/Pester/wiki/Code-Coverage) for more details. 288 | 289 | ---------- 290 | 291 | ### Releasing 292 | 293 | If you are a maintainer: 294 | 295 | Ensure that the version number of the module is updated with every pull request that is being 296 | accepted. 297 | 298 | This project follows [semantic versioning](http://semver.org/) in the following way: 299 | 300 | .. 301 | 302 | Where: 303 | * `` - Changes only with _significant_ updates. 304 | * `` - If this is a feature update, increment by one and be sure to reset `` to 0. 305 | * `` - If this is a bug fix, leave `` alone and increment this by one. 306 | 307 | When new code changes are checked in to the repo, a new NuGet package must be published by Microsoft. 308 | This process is documented in Microsoft's internal StoreBroker repo. 309 | Refer to the CONTRIBUTING.md in that repo for more information on creating a signed NuGet package. 310 | 311 | Once the new version has been pulled into master, there are two additional tasks to perform: 312 | * Update [CHANGELOG.md](./CHANGELOG.md) 313 | * Add a tag for that version to the repo 314 | 315 | #### Updating the CHANGELOG 316 | To update [CHANGELOG.md](./CHANGELOG.md), just duplicate the previous section and update it to be 317 | relevant for the new release. Be sure to update all of the sections: 318 | * The version number 319 | * The SB tree (we'll get that path working in a moment) 320 | * The release date 321 | * A brief list of all the changes (use a `-` for the bullet point if it's fixing a bug, or a `+` for a feature) 322 | * The link to the pull request (pr) (so that the discussion on the change can be easily reviewed) and the changelist (cl) 323 | * The author (and a link to their profile) 324 | * If it's a new contributor, also add them to the [Contributors](#contributors) list below. 325 | 326 | Then get a new pull request out for that change to CHANGELOG. 327 | 328 | #### Adding a New Tag 329 | To add a new tag: 330 | 1. Make sure that you're in a clone of the actual repo and not your own private fork. 331 | 2. Make sure that you have checked out `master` and that it's fully up-to-date 332 | 3. Run `git tag -a '' 333 | 4. In the pop-up editor, give a one-line summary of the change (that you possibly already wrote for the CHANGELOG) 334 | 5. Save and close the editor 335 | 6. Run `git push --tags` to upload the new tag you just created 336 | 337 | If you want to make sure you get these tags on any other forks/clients, you can run 338 | `git fetch origin --tags` or `git fetch upstream --tags`, or whatever you've named the source to be. 339 | 340 | ---------- 341 | 342 | ### Contributors 343 | 344 | Thank you to all of our contributors, no matter how big or small the contribution: 345 | 346 | - **[Howard Wolosky (@HowardWolosky)](http://github.com/HowardWolosky)** 347 | - **[Daniel Belcher (@DanBelcher-MSFT)](http://github.com/danbelcher-MSFT)** 348 | - **[Lisa Ong (@lisaong)](http://github.com/LisaOng)** 349 | - **[Alex Dobin (@adobin)](http://github.com/ADobin)** 350 | - **[Petr Bartoň (@Pebin)](http://github.com/Pebin)** 351 | - **[Matt Perzel (@mattperzel)](http://github.com/mattperzel)** 352 | - **[Josh Wisniewski (@jowis41)](http://github.com/jowis41)** 353 | - **[Luke Cartwright (@cartwrightluke)](https://github.com/cartwrightluke)** 354 | - **[Rafael Rivera (@riverar)](https://github.com/riverar)** 355 | 356 | ---------- 357 | 358 | ### Legal and Licensing 359 | 360 | StoreBroker is licensed under the [MIT license](..\LICENSE). 361 | 362 | You will need to complete a Contributor License Agreement (CLA) for any code submissions. 363 | Briefly, this agreement testifies that you are granting us permission to use the submitted change 364 | according to the terms of the project's license, and that the work being submitted is under 365 | appropriate copyright. You only need to do this once. 366 | 367 | When you submit a pull request, [@msftclas](https://github.com/msftclas) will automatically 368 | determine whether you need to sign a CLA, comment on the PR and label it appropriately. 369 | If you do need to sign a CLA, please visit https://cla.microsoft.com and follow the steps. 370 | -------------------------------------------------------------------------------- /StoreBroker/AppConfigTemplate.json: -------------------------------------------------------------------------------- 1 | // Copyright (C) Microsoft Corporation. All rights reserved. 2 | 3 | // This configuration file is for use with the StoreBroker PowerShell module. 4 | // Configure details of the app here, as well as the location of resources 5 | // used by the packaging tool. 6 | 7 | // Inline Comments can be placed in this file using the '//' delimiter. 8 | // The delimiter and any remaining text on that line will be removed. 9 | 10 | { 11 | // New-SubmissionPackage parameters 12 | // 13 | // If you notice one of the parameters you specify to New-SubmissionPackage does not change, you can specify it here 14 | // in order to avoid providing it at runtime. If New-SubmissionPackage does not have a parameter it needs, it will 15 | // check the provided config file for a non-null, non-empty value. The exception to this is -ConfigPath which must 16 | // always be provided. For information on these parameters see the README.md or run `help New-SubmissionPackage -ShowWindow` 17 | // 18 | // WARNING: Specifying a parameter at runtime will ignore any value placed in the config file. 19 | "packageParameters": { 20 | 21 | // There are two supported layouts for your PDP files: 22 | // 1. \\...\PDP.xml 23 | // 2. \\\...\PDP.xml 24 | // The only difference between these two is that there is a directory after the 25 | // and before the sub-directories. 26 | // 27 | // The first layout is generally used when your localization system will be downloading 28 | // the localized PDP files during your build. In that situation, it's always retrieving 29 | // the latest version. Alternatively, if the latest localized versions of your PDP 30 | // files are always stored in the same location, this layout is also for you. 31 | // 32 | // On the other hand, if you will be archiving the localized PDP's based on each release 33 | // to the Store, then the second layout is the one that you should use. In this scenario, 34 | // you will specify the value of "" immediately below, or at the commandline. 35 | // 36 | // Only specify 'PDPRootPath' here if it does not change. 37 | // DO NOT use environment variables in the config. If you need to use an environment 38 | // variable to get to the right path, then you need to specify that path at the commandline 39 | // instead. 40 | // 41 | // File separator '\' must be escaped, i.e. '\\' 42 | // Ex: \\\\filehare\\Public\\AppPDPs\\MyApp 43 | "PDPRootPath": "", 44 | 45 | // If your PDP files are being placed in a directory structure like this: 46 | // \\\...\PDP.xml 47 | // then specify the correct 'Release' here or at runtime. 48 | "Release": "", 49 | 50 | // Filenames that SHOULD be processed. 51 | // Wildcards are allowed, eg "ProductDescription*.xml". 52 | // It is fine to specify both "PDPInclude" and "PDPExclude". 53 | // 54 | // Ex: "PDPInclude": [ 55 | // "PDP*.xml", <- Comma to separate items 56 | // "ProductDescription.xml" <- No comma for last item, or JSON deserialization will fail 57 | // ] 58 | "PDPInclude": [ 59 | ], 60 | 61 | // Filenames that SHOULD NOT be processed. 62 | // Wildcards are allowed, eg "ProductDescription*.xml". 63 | // It is fine to specify both "PDPInclude" and "PDPExclude". 64 | // It is NOT necessary to specify something here, but you can to be more explicit. 65 | // 66 | // Ex: "PDPExclude": [ 67 | // "*.xml.lss", <- Comma to separate items 68 | // "*.xml.lct" <- No comma for last item, or JSON deserialization will fail 69 | // ] 70 | "PDPExclude": [ 71 | ], 72 | 73 | // Languages to be excluded from processing by New-SubmissionPackage. 74 | // New-SubmissionPackage will use the filepath of a PDP XML file 75 | // to identify the language of the metadata. If it sees a match 76 | // with a lang-code in this list, it will skip processing the file. 77 | // 78 | // Ex: "LanguageExclude": [ 79 | // "default", <- Comma to separate items 80 | // "qps-ploc" <- No comma for last item, or JSON deserialization will fail 81 | // ], 82 | "LanguageExclude": [ 83 | "default" 84 | ], 85 | 86 | // Your store screenshots must be placed with this structure: 87 | // \\\...\img.png 88 | // Specify 'ImagesRootPath' here. 89 | // 90 | // IMPORTANT: The 'Release' that will be used here is *NOT* the value specified to 91 | // New-SubmissionPackage (nor is it the Release value specified earlier in this config file), 92 | // but rather the 'Release' attribute at the top of the corresponding PDP file. 93 | // 94 | // File separator '\' must be escaped, i.e. '\\' 95 | // Ex: \\\\fileshare\\Public\\Screenshots\\MyApp 96 | "ImagesRootPath": "", 97 | 98 | // Some apps may not localize all of their metadata media (images, trailers, etc..) across all languages. 99 | // By default, StoreBroker will look in the PDP langcode's subfolder within ImagesRootPath for that 100 | // language's media content. If the requested filename is not found, StoreBroker packaging will fail. 101 | // If you specify a fallback language here (e.g. 'en-us'), then if the requested file isn't found in 102 | // the PDP language's media subfolder, StoreBroker will then look into the fallback language's media 103 | // subfolder for the exactly same-named image before failing. 104 | "MediaFallbackLanguage": "", 105 | 106 | // Specify any number of full paths to .appx, .appxbundle, or .appxupload files here. 107 | // Ex: 108 | // "AppxPath": [ 109 | // "C:\app\x86\MyApp_x86.appxupload", <- Comma to separate items 110 | // "C:\app\x64\MyApp_x64.appxupload" <- No comma for last item, or JSON deserialization will fail 111 | // ] 112 | // 113 | // DO NOT use environment variables in the config. If you need to use an environment 114 | // variable to get to the right path, then you need to specify that path at the commandline 115 | // instead. In order to specify a list of paths at the commandline, just separate them 116 | // by commas and but the whole list in parenthesis: 117 | // Ex: ("C:\app\x86\MyApp_x86.appxupload", "C:\app\x64\MyApp_x64.appxupload") 118 | // 119 | // NOTE: File separator '\' must be escaped, i.e. '\\' 120 | // Ex: \\\\fileshare\\Public\\AppPDPs\\MyApp 121 | "AppxPath": [ 122 | ], 123 | 124 | // Full path to a directory where the Packaging Tool can write the .json submission request 125 | // body and .zip package to upload. 126 | // 127 | // File separator '\' must be escaped, i.e. '\\' 128 | // Ex: "OutPath": "C:\\app\\output" 129 | "OutPath": "", 130 | 131 | // Common name to give to the .json and .zip files outputted by the Packaging Tool. 132 | "OutName": "", 133 | 134 | // By default, the packages will be renamed using a consistent naming scheme, which 135 | // embeds the application name, version, as well as targeted platform and architecture. 136 | // Ex: "Desktop_MyApp_2.13.22002.0_x86.appxupload" 137 | // To retain the existing package filenames instead, set this to true. 138 | "DisableAutoPackageNameFormatting": false 139 | }, 140 | 141 | // Configure details of the App Submission 142 | 143 | "appSubmission": { 144 | 145 | // If known, this is the AppId given to your application by the Windows Store. 146 | // This isn't required, but adding this will help protect you from submitting something 147 | // to the wrong application when working at the commandline. 148 | // 149 | // Ex: "appId": "0ABCDEF12345" 150 | "appId": "", 151 | 152 | ///////////////////////////////// 153 | // // 154 | // PUBLISH MODE AND VISIBILITY // 155 | // // 156 | ///////////////////////////////// 157 | 158 | // Publish mode of the submission. One of ["NotSet", "Immediate", "Manual", "SpecificDate"] 159 | "targetPublishMode": "NotSet", 160 | 161 | // Publish date-time of the submission if the "targetPublishMode" is specified as "SpecificDate". 162 | // Date-time format should follow the ISO 8601 standard. 163 | // Ex: "2000-01-30T00:00:00-08:00" 164 | "targetPublishDate": null, 165 | 166 | // Visibility of the app. One of ["NotSet", "Public", "Private", "Hidden"] 167 | // 168 | // Public: Anyone can find your app in the Store. 169 | // Private: Hide this app in the Store. Customers with a direct link to the app's listing can still download 170 | // it, except on Windows 8 and Windows 8.1. 171 | // Hidden: Hide this app and prevent acquisition. Customers with a promotional code can still download it on 172 | // Windows 10 devices. 173 | "visibility": "NotSet", 174 | 175 | ////////////////////////////// 176 | // // 177 | // PRICING AND AVAILABILITY // 178 | // // 179 | ////////////////////////////// 180 | 181 | // Pricing 182 | // 183 | // Your app will be priced according to the price tier set for "priceId" and will be marketed at 184 | // that tier for ALL available markets. To set a different price for a specific market, use the 185 | // "marketSpecificPricings" object. You can also use the "marketSpecificPricings" object to make 186 | // your app "NotAvailable" to a specific market. 187 | // 188 | // If your app is having a sale, set that here using the "sales" object. 189 | "pricing": { 190 | 191 | // Price 192 | // 193 | // Set the price of the app. 194 | // The value provided should be a "Tier" and NOT an actual price 195 | // For a mapping of "Tiers" to prices, refer to https://docs.microsoft.com/en-us/windows/uwp/monetize/manage-add-on-submissions#price-tiers 196 | // Can also be "Free" or "NotAvailable" 197 | "priceId": "NotAvailable", 198 | 199 | // Trial period. One of ["NoFreeTrial", "OneDay", "SevenDays", "FifteenDays", "ThirtyDays", "TrialNeverExpires"] 200 | "trialPeriod": "NoFreeTrial", 201 | 202 | // Market Specific Pricing 203 | // 204 | // By default, your app will be marketed at the price tier set for "priceId" for all possible markets. 205 | // To set a different price tier for a specific region, add the ("region": "Tier") key-value pair. 206 | // 207 | // For a list of supported regions, see https://msdn.microsoft.com/en-us/library/windows/apps/mt148563.aspx 208 | "marketSpecificPricings": { 209 | }, 210 | 211 | // Sales 212 | // 213 | // Sales follow the same pattern as the general "pricing" object. Specify a "basePriceId" and the 214 | // app will be on sale at that price tier for ALL possible markets. To specify a different price tier 215 | // for a specific region, or to exclude a region from the sale ("NotAvailable"), use "marketSpecificPricings" 216 | // exactly as you would above. Specify a "startDate" and "endDate" according to the ISO 8601 217 | // e.g. "2000-01-30T00:00:00-08:00". Finally, give a "name" for the sale, to identify it in the portal. 218 | // 219 | // Specify any number of "Sale" objects here. A "Sale" object has the following format: 220 | // 221 | // { <- Open-curly to start the object 222 | // "name": "", 223 | // "basePriceId": "", 224 | // "marketSpecificPricings": { 225 | // }, 226 | // "startDate": "", 227 | // "endDate": "" <- No comma for last item, or JSON deserialization will fail 228 | // } <- Closing-curly to end the object 229 | // 230 | // 231 | // Ex: 232 | // "sales": [ 233 | // { 234 | // "name": "Sale1", 235 | // "basePriceId": "Free", 236 | // "marketSpecificPricings": { 237 | // "ca-es": "NotAvailable", <- Comma to separate key-value pairs 238 | // "es-mx": "Tier2" <- No comma for last item, or JSON deserialization will fail 239 | // }, 240 | // "startDate": "2000-01-30T00:00:00-08:00", 241 | // "endDate": "2000-02-07T00:00:00-08:00" 242 | // }, <- Comma to separate "Sale" objects 243 | // { 244 | // "name": "Sale2", 245 | // "basePriceId": "Tier1", 246 | // "marketSpecificPricings": { 247 | // "en-gb": "NotAvailable", 248 | // "fr-ca": "NotAvailable", 249 | // "es-mx": "Tier2" 250 | // }, 251 | // "startDate": "2000-03-01T00:00:00-08:00", 252 | // "endDate": "2001-01-01T00:00:00-08:00" 253 | // } <- No comma for last item, or JSON deserialization will fail 254 | // ] 255 | "sales": [ 256 | ] 257 | }, 258 | 259 | // Define the preference to make this app available for each Win10Device 260 | "allowTargetFutureDeviceFamilies": { 261 | "Xbox": false, 262 | "Team": false, // Surface Hub 263 | "Holographic": false, 264 | "Desktop": false, 265 | "Mobile": false 266 | }, 267 | 268 | // True, Let Microsoft decide whether to make this app available to any future device families. 269 | // False, Require your permission before making this app available to any future device families. 270 | "allowMicrosoftDecideAppAvailabilityToFutureDeviceFamilies": false, 271 | 272 | // EnterpriseLicensing. One of ["None", "Online", "OnlineAndOffline"] 273 | // 274 | // You can allow organizations to acquire your app in volume through the options below. 275 | // Note that changes will only affect new acquisitions; anyone who already has your app 276 | // will be able to continue using it. Initially, only free apps will be offered for volume 277 | // acquisition. Selections you make for paid apps will take effect once support is added 278 | // for acquiring them in volume at a later date. 279 | // 280 | // Online: Make app available to organizations with Store-managed (online) volume licensing. 281 | // This allows organizations to acquire your app in volume. App licenses will be managed 282 | // through the Store's online licensing system. 283 | // OnlineAndOffline: In addition, allow disconnected (offline) licensing for organizations. 284 | // This allows organizations to acquire your app in volume. They can then download your 285 | // package and a license which lets them install it to devices without accessing the 286 | // Store's online licensing system. 287 | "enterpriseLicensing": "None", 288 | 289 | //////////////////// 290 | // // 291 | // APP PROPERTIES // 292 | // // 293 | //////////////////// 294 | 295 | // Category/SubCategory of the submission. Either 296 | // "NotSet" 297 | // or one of: 298 | // ["BooksAndReference", "BooksAndReference_EReader", "BooksAndReference_Fiction", "BooksAndReference_Nonfiction", 299 | // "BooksAndReference_Reference", "Business", "Business_AccountingAndFinance", "Business_Collaboration", 300 | // "Business_CRM", "Business_DataAndAnalytics", "Business_FileManagement", "Business_InventoryAndLogistics", 301 | // "Business_LegalAndHR", "Business_ProjectManagement", "Business_RemoteDesktop", "Business_SalesAndMarketing", 302 | // "Business_TimeAndExpenses", "DeveloperTools", "DeveloperTools_Database", "DeveloperTools_DesignTools", 303 | // "DeveloperTools_DevelopmentKits", "DeveloperTools_Networking", "DeveloperTools_ReferenceAndTraining", 304 | // "DeveloperTools_Servers", "DeveloperTools_Utilities", "DeveloperTools_Webhosting", "Education", 305 | // "Education_BooksAndReference", "Education_EarlyLearning", "Education_InstructionalTools", "Education_Language", 306 | // "Education_StudyAids", "Entertainment", "FoodAndDining", "Games", "Games_ActionAndAdventure", 307 | // "Games_CardAndBoard", "Games_Casino", "Games_Educational", "Games_FamilyAndKids", "Games_Fighting", 308 | // "Games_Music", "Games_Platformer", "Games_PuzzleAndTrivia", "Games_RacingAndFlying", "Games_Roleplaying", 309 | // "Games_Shooter", "Games_Simulation", "Games_Sports", "Games_Strategy", "Games_Word", "GovernmentAndPolitics", 310 | // "HealthAndFitness", "KidsAndFamily", "KidsAndFamily_BooksAndReference", "KidsAndFamily_Entertainment", 311 | // "KidsAndFamily_HobbiesAndToys", "KidsAndFamily_SportsAndActivities", "KidsAndFamily_Travel", "Lifestyle", 312 | // "Lifestyle_Automotive", "Lifestyle_DIY", "Lifestyle_HomeAndGarden", "Lifestyle_Relationships", 313 | // "Lifestyle_SpecialInterest", "Lifestyle_StyleAndFashion", "Medical", "MultimediaDesign", 314 | // "MultimediaDesign_IllustrationAndGraphicDesign", "MultimediaDesign_MusicProduction", 315 | // "MultimediaDesign_PhotoAndVideoProduction", "Music", "NavigationAndMaps", "NewsAndWeather", 316 | // "NewsAndWeather_News", "NewsAndWeather_Weather", "PersonalFinance", "PersonalFinance_BankingAndInvestments", 317 | // "PersonalFinance_BudgetingAndTaxes", "Personalization", "Personalization_RingtonesAndSounds", 318 | // "Personalization_Themes", "Personalization_WallpaperAndLockScreens", "PhotoAndVideo", "Productivity", 319 | // "Security", "Security_PCProtection", "Security_PersonalSecurity", "Shopping", "Social", "Sports", "Travel", 320 | // "Travel_CityGuides", "Travel_Hotels", "UtilitiesAndTools", "UtilitiesAndTools_BackupAndManage" 321 | // "UtilitiesAndTools_FileManager"] 322 | "applicationCategory": "NotSet", 323 | 324 | // Hardware (Preferences) 325 | // 326 | // Any of ["Undefined", "Touch", "Keyboard", "Mouse", "Camera", "Nfc", "NfcHce", "BluetoothLE", "Telephony"] 327 | // Ex: "hardwarePreferences": [ 328 | // "Touch", 329 | // "Mouse" <- No comma for last item, or JSON deserialization will fail 330 | // ], 331 | 332 | // Hardware Preferences 333 | // 334 | // Indicate which hardware features are required in order for your app to run properly. 335 | // Customers on hardware that doesn't meet your app's preferences will see a warning before they download your app, 336 | // and they won't be able to rate or review it. 337 | "hardwarePreferences": [ 338 | ], 339 | 340 | // Indicates whether the app sells In-App-Products from 3rd parties. 341 | "hasExternalInAppProducts": false, 342 | 343 | // Indicates whether the App meets accessibility guidelines. 344 | "meetAccessibilityGuidelines": false, 345 | 346 | // Indicates whether the app can be installed on removable media. 347 | "canInstallOnRemovableMedia": false, 348 | 349 | // Indicates whether OneDrive backup is enabled on the app instance being submitted. 350 | "automaticBackupEnabled": false, 351 | 352 | // Indicates whether customers can use Windows 10 features to record and broadcast clips of this game. 353 | // Only supported for app in the "Games" category. 354 | "isGameDvrEnabled": false, 355 | 356 | ///////////////////// 357 | // // 358 | // GAMING OPTIONS // 359 | // // 360 | ///////////////////// 361 | 362 | // Up-to-date documentation for all possible option values can be found here: 363 | // https://docs.microsoft.com/en-us/windows/uwp/monetize/manage-app-submissions#gaming-options-object 364 | "gamingOptions": [{ 365 | // An array of one or more of the following strings that describe the genres of the game: 366 | // ["Games_ActionAndAdventure", "Games_CardAndBoard", "Games_Casino", "Games_Educational", "Games_FamilyAndKids", 367 | // "Games_Fighting", "Games_Music", "Games_Platformer", "Games_PuzzleAndTrivia", "Games_RacingAndFlying", 368 | // "Games_RolePlaying", "Games_Shooter", "Games_Simulation", "Games_Sports", "Games_Strategy", "Games_Word"] 369 | "genres": [ 370 | ], 371 | 372 | // Indicates whether the game supports local multiplayer. 373 | "isLocalMultiplayer": false, 374 | 375 | // Indicates whether the game supports local co-op. 376 | "isLocalCooperative": false, 377 | 378 | // Indicates whether the game supports online multiplayer. 379 | "isOnlineMultiplayer": false, 380 | 381 | // Indicates whether the game supports online co-op. 382 | "isOnlineCooperative": false, 383 | 384 | // Specifies the minimum number of players the game supports for local multiplayer. 385 | "localMultiplayerMinPlayers": 0, 386 | 387 | // Specifies the maximum number of players the game supports for local multiplayer. 388 | "localMultiplayerMaxPlayers": 0, 389 | 390 | // Specifies the minimum number of players the game supports for local co-op. 391 | "localCooperativeMinPlayers": 0, 392 | 393 | // Specifies the maximum number of players the game supports for local co-op. 394 | "localCooperativeMaxPlayers": 0, 395 | 396 | // Indicates whether the game supports broadcasting. 397 | "isBroadcastingPrivilegeGranted": false, 398 | 399 | // Indicates whether the game supports multiplayer sessions between players on Windows 10 PCs and Xbox. 400 | "isCrossPlayEnabled": false, 401 | 402 | // One of the following string values that indicates whether the game can collect Kinect data and send it to external services: 403 | // ["NotSet", "Unknown", "Enabled", "Disabled"] 404 | "kinectDataForExternal": "Disabled" 405 | }], 406 | 407 | ///////////////////////////// 408 | // // 409 | // NOTES FOR CERTIFICATION // 410 | // // 411 | ///////////////////////////// 412 | 413 | // String. Information that testers can use to run the app for certification. 414 | // Can include information such as test account credentials, steps to access and verify features, etc. 415 | // 416 | // The notes must be provided on one line, but it is possible to specify multiline notes using "\r\n". 417 | // Ex: "notesForCertification": "These\r\nare\r\nfour\r\nlines" 418 | "notesForCertification": "" 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /Extensions/ConvertFrom-ExistingIapSubmission.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (C) Microsoft Corporation. All rights reserved. 2 | 3 | <# 4 | .SYNOPSIS 5 | Script for converting an existing In-AppProduct (IAP) submission in the Store 6 | to the January 2017 PDP schema. 7 | 8 | .DESCRIPTION 9 | Script for converting an existing In-AppProduct (IAP) submission in the Store 10 | to the January 2017 PDP schema. 11 | 12 | The Git-repo for the StoreBroker module can be found here: http://aka.ms/StoreBroker 13 | 14 | .PARAMETER IapId 15 | The ID of the IAP that the PDP's will be getting created for. 16 | The most recent submission for this IAP will be used unless a SubmissionId is 17 | explicitly specified. 18 | 19 | .PARAMETER SubmissionId 20 | The ID of the application submission that the PDP's will be getting created for. 21 | The most recent submission for IapId will be used unless a value for this parameter is 22 | provided. 23 | 24 | .PARAMETER SubmissionId 25 | The submission object that you want to convert, which was previously retrieved. 26 | 27 | .PARAMETER Release 28 | The release to use. This value will be placed in each new PDP and used in conjunction with '-OutPath'. 29 | Some examples could be "1601" for a January 2016 release, "March 2016", or even just "1". 30 | 31 | .PARAMETER PdpFileName 32 | The name of the PDP file that will be generated for each region. 33 | 34 | .PARAMETER OutPath 35 | The output directory. 36 | This script will create two subfolders of OutPath: 37 | \PDPs\\ 38 | \Images\\ 39 | Each of these sub-folders will have region-specific subfolders for their file content. 40 | 41 | .EXAMPLE 42 | .\ConvertFrom-ExistingIapSubmission -IapId 0ABCDEF12345 -Release "March Release" -OutPath "C:\NewPDPs" 43 | 44 | Converts the data from the last published submission for IapId 0ABCDEF12345. The generated files 45 | will use the default name of "PDP.xml" and be located in lang-code specific sub-directories within 46 | c:\NewPDPs. 47 | 48 | .EXAMPLE 49 | .\ConvertFrom-ExistingIapSubmission -IapId 0ABCDEF12345 -SubmissionId 1234567890123456789 -Release "March Release" -PdpFileName "InAppProductDescription.xml" -OutPath "C:\NewPDPs" 50 | 51 | Converts the data from submission 1234567890123456789 for IapId 0ABCDEF12345 (which might be a 52 | published or pending submission). The generated files will be named "InAppProductDescription.xml" and 53 | will be located in lang-code specific sub-directories within c:\NewPDPs. 54 | 55 | .EXAMPLE 56 | .\ConvertFrom-ExistingIapSubmission -Submission $sub -Release "March Release" -OutPath "C:\NewPDPs" 57 | 58 | Converts the data from a submission object that was captured earlier in your PowerShell session. 59 | It might have come from Get-InAppProductSubmission, or it might have been generated some other way. 60 | This method of running the script was created more for debugging purposes, but others may find it 61 | useful. The generated files will use the default name of "PDP.xml" and be located in lang-code 62 | specific sub-directories within c:\NewPDPs. 63 | #> 64 | [CmdletBinding( 65 | SupportsShouldProcess, 66 | DefaultParametersetName = "UseApi")] 67 | param( 68 | [Parameter( 69 | Mandatory, 70 | ParameterSetName = "UseApi", 71 | Position = 0)] 72 | [string] $IapId, 73 | 74 | [Parameter( 75 | ParameterSetName = "UseApi", 76 | Position = 1)] 77 | [string] $SubmissionId = $null, 78 | 79 | [Parameter( 80 | Mandatory, 81 | ParameterSetName = "ProvideSubmission", 82 | Position = 0)] 83 | [PSCustomObject] $Submission = $null, 84 | 85 | [Parameter(Mandatory)] 86 | [string] $Release, 87 | 88 | [string] $PdpFileName = "PDP.xml", 89 | 90 | [Parameter(Mandatory)] 91 | [string] $OutPath 92 | ) 93 | 94 | # Import Write-Log 95 | $rootDir = Split-Path -Path $PSScriptRoot -Parent 96 | $helpers = "$rootDir\StoreBroker\Helpers.ps1" 97 | if (-not (Test-Path -Path $helpers -PathType Leaf)) 98 | { 99 | throw "Script execution requires Helpers.ps1 which is part of the git repo. Please execute this script from within your cloned repo." 100 | } 101 | . $helpers 102 | 103 | #region Comment Constants 104 | $script:LocIdAttribute = "_locID" 105 | $script:LocIdFormat = "Iap_{0}" 106 | $script:CommentFormat = " _locComment_text=`"{{MaxLength={0}}} {1}`" " 107 | #endregion Comment Constants 108 | 109 | function Add-ToElement 110 | { 111 | <# 112 | .SYNOPSIS 113 | Adds an arbitrary number of comments and attributes to an XmlElement. 114 | 115 | .PARAMETER Element 116 | The XmlElement to be modified. 117 | 118 | .PARAMETER Comment 119 | An array of comments to add to the element. 120 | 121 | .PARAMETER Attribute 122 | A hashtable where the keys are the attribute names and the values are the attribute values. 123 | 124 | .NOTES 125 | If a provided attribute already exists on the Element, the Element will NOT be modified. 126 | It will ONLY be modified if the Element does not have that attribute. 127 | 128 | .EXAMPLE 129 | PS C:\>$xml = [xml] (Get-Content $filePath) 130 | PS C:\>$root = $xml.DocumentElement 131 | PS C:\>Add-ToElement -Element $root -Comment "Comment1", "Comment2" -Attribute @{ "Attrib1"="Val1"; "Attrib2"="Val2" } 132 | 133 | Adds two comments and two attributes to the root element of the XML document. 134 | 135 | #> 136 | param( 137 | [Parameter(Mandatory)] 138 | [System.Xml.XmlElement] $Element, 139 | 140 | [string[]] $Comment, 141 | 142 | [hashtable] $Attribute 143 | ) 144 | 145 | if ($Comment.Count -gt 1) 146 | { 147 | # Reverse 'Comment' array in order to preserve order because of nature of 'Prepend' 148 | # Input array is modified in place, no need to capture result 149 | [Array]::Reverse($Comment) 150 | } 151 | 152 | foreach ($text in $Comment) 153 | { 154 | if (-not [String]::IsNullOrWhiteSpace($text)) 155 | { 156 | $elem = $Element.OwnerDocument.CreateComment($text) 157 | $Element.PrependChild($elem) | Out-Null 158 | } 159 | } 160 | 161 | foreach ($key in $Attribute.Keys) 162 | { 163 | if ($null -eq $Element.$key) 164 | { 165 | $Element.SetAttribute($key, $Attribute[$key]) 166 | } 167 | else 168 | { 169 | $out = "For element $($Element.LocalName), did not create attribute '$key' with value '$($Attribute[$key])' because the attribute already exists." 170 | Write-Log -Message $out -Level Warning 171 | } 172 | } 173 | } 174 | 175 | function Ensure-RootChild 176 | { 177 | <# 178 | .SYNOPSIS 179 | Creates the specified element as a child of the XML root node, only if that element does not exist already. 180 | 181 | .PARAMETER Xml 182 | The XmlDocument to (potentially) modify. 183 | 184 | .PARAMETER Element 185 | The name of the element to existence check. 186 | 187 | .OUTPUTS 188 | XmlElement. Returns a reference to the (possibly newly created) element requested. 189 | 190 | .EXAMPLE 191 | PS C:\>$xml = [xml] (Get-Content $filePath) 192 | PS C:\>Ensure-RootChild -Xml $xml -Element "SomeElement" 193 | 194 | $xml.DocumentElement.SomeElement now exists and is an XmlElement object. 195 | #> 196 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 197 | param( 198 | [Parameter(Mandatory)] 199 | [System.Xml.XmlDocument] $Xml, 200 | 201 | [Parameter(Mandatory)] 202 | [string] $Element 203 | ) 204 | 205 | # ProductDescription node 206 | $root = $Xml.DocumentElement 207 | 208 | if ($root.GetElementsByTagName($Element).Count -eq 0) 209 | { 210 | $elem = $Xml.CreateElement($Element, $Xml.DocumentElement.NamespaceURI) 211 | $root.AppendChild($elem) | Out-Null 212 | } 213 | 214 | return $root.GetElementsByTagName($Element)[0] 215 | } 216 | 217 | function Add-Title 218 | { 219 | <# 220 | .SYNOPSIS 221 | Creates the Title node. 222 | 223 | .PARAMETER Xml 224 | The XmlDocument to modify. 225 | 226 | .PARAMETER Listing 227 | The base listing from the submission for a specific Lang. 228 | #> 229 | param( 230 | [Parameter(Mandatory)] 231 | [System.Xml.XmlDocument] $Xml, 232 | 233 | [Parameter(Mandatory)] 234 | [PSCustomObject] $Listing 235 | ) 236 | 237 | $elementName = "Title" 238 | $elementNode = Ensure-RootChild -Xml $Xml -Element $elementName 239 | $elementNode.InnerText = $Listing.title 240 | 241 | # Add comment to parent 242 | $maxChars = 100 243 | $paramSet = @{ 244 | "Element" = $elementNode; 245 | "Attribute" = @{ $script:LocIdAttribute = ($script:LocIdFormat -f $elementName) }; 246 | "Comment" = @( 247 | " [required] ", 248 | ($script:CommentFormat -f $maxChars, "IAP $elementName")) 249 | } 250 | 251 | Add-ToElement @paramSet 252 | } 253 | 254 | function Add-Description 255 | { 256 | <# 257 | .SYNOPSIS 258 | Creates the description node 259 | 260 | .PARAMETER Xml 261 | The XmlDocument to modify. 262 | 263 | .PARAMETER Listing 264 | The base listing from the submission for a specific Lang. 265 | #> 266 | param( 267 | [Parameter(Mandatory)] 268 | [System.Xml.XmlDocument] $Xml, 269 | 270 | [Parameter(Mandatory)] 271 | [PSCustomObject] $Listing 272 | ) 273 | 274 | $elementName = "Description" 275 | $elementNode = Ensure-RootChild -Xml $Xml -Element $elementName 276 | $elementNode.InnerText = $Listing.description 277 | 278 | # Add comment to parent 279 | $maxChars = 200 280 | $paramSet = @{ 281 | "Element" = $elementNode; 282 | "Attribute" = @{ $script:LocIdAttribute = ($script:LocIdFormat -f $elementName) }; 283 | "Comment" = @( 284 | " [optional] ", 285 | ($script:CommentFormat -f $maxChars, "IAP $elementName")) 286 | } 287 | 288 | Add-ToElement @paramSet 289 | } 290 | 291 | function Add-Icon 292 | { 293 | <# 294 | .SYNOPSIS 295 | Creates the icon node. 296 | 297 | .PARAMETER Xml 298 | The XmlDocument to modify. 299 | 300 | .PARAMETER Listing 301 | The base listing from the submission for a specific Lang. 302 | 303 | .OUTPUTS 304 | [String] The path specified for the icon (if available) 305 | 306 | .NOTES 307 | This function is implemented a bit differently than the others. 308 | Icon is an optional element, but if it's specified, the Filename attribute must 309 | be included with a non-empty value. This is fine if the Listing has an icon defined, 310 | but if it doesn't, then we want to include the element, but commented out so that users 311 | know later what they need to add if they wish to include an icon at some future time. 312 | We will create/add the icon node the way we do in all other cases so that we have its XML, 313 | but if we then determine that there is no icon for that listing, we'll convert the element 314 | to its XML string representation that we can add as a comment, and then remove the actual 315 | node. 316 | #> 317 | param( 318 | [Parameter(Mandatory)] 319 | [System.Xml.XmlDocument] $Xml, 320 | 321 | [Parameter(Mandatory)] 322 | [PSCustomObject] $Listing 323 | ) 324 | 325 | # For this element, we want the comment above, rather than inside, the element. 326 | $comment = $Xml.CreateComment(' [optional] Specifying an icon is optional. If provided, the icon must be a 300 x 300 png file. ') 327 | $Xml.DocumentElement.AppendChild($comment ) | Out-Null 328 | 329 | $iconFilename = $Listing.icon.fileName 330 | 331 | $elementName = "Icon" 332 | [System.Xml.XmlElement] $elementNode = Ensure-RootChild -Xml $Xml -Element $elementName 333 | 334 | $paramSet = @{ 335 | "Element" = $elementNode; 336 | "Attribute" = @{ 'Filename' = $iconFilename }; 337 | } 338 | 339 | Add-ToElement @paramSet 340 | 341 | if ($null -eq $iconFilename) 342 | { 343 | # We'll just comment this out for now since it's not being used. 344 | # We do a tiny bit of extra processing to remove the unnecessary xmlns attribute that 345 | # is added to the node when we get the OuterXml. 346 | $iconElementXml = $elementNode.OuterXml -replace 'xmlns="[^"]+"', "" 347 | $comment = $Xml.CreateComment(" $iconElementXml ") 348 | $Xml.DocumentElement.RemoveChild($elementNode) | Out-Null 349 | $Xml.DocumentElement.AppendChild($comment ) | Out-Null 350 | } 351 | 352 | return $iconFilename 353 | } 354 | 355 | function ConvertFrom-Listing 356 | { 357 | <# 358 | .SYNOPSIS 359 | Converts a base listing for an existing submission into a PDP file that conforms with 360 | the January 2017 PDP schema. 361 | 362 | .PARAMETER Listing 363 | The base listing from the submission for the indicated Lang. 364 | 365 | .PARAMETER Lang 366 | The language / region code for the PDP (e.g. "en-us") 367 | 368 | .PARAMETER Release 369 | The release to use. This value will be placed in each new PDP. 370 | Some examples could be "1601" for a January 2016 release, "March 2016", or even just "1". 371 | 372 | .PARAMETER PdpRootPath 373 | The root / base path that all of the language sub-folders will go for the PDP files. 374 | 375 | .PARAMETER FileName 376 | The name of the PDP file that will be generated. 377 | 378 | .OUTPUTS 379 | [String[]] Array of image names that the PDP references 380 | 381 | .EXAMPLE 382 | ConvertFrom-Listing -Listing ($sub.listings."en-us".baseListing) -Lang "en-us" -Release "1701" -PdpRootPath "C:\PDPs\" -FileName "PDP.xml" 383 | 384 | Converts the given "en-us" base listing to the current PDP schema, 385 | and saves it to "c:\PDPs\en-us\PDP.xml" 386 | #> 387 | [CmdletBinding()] 388 | param( 389 | [Parameter(Mandatory)] 390 | [PSCustomObject] $Listing, 391 | 392 | [Parameter(Mandatory)] 393 | [string] $Lang, 394 | 395 | [Parameter(Mandatory)] 396 | [string] $Release, 397 | 398 | [Parameter(Mandatory)] 399 | [string] $PdpRootPath, 400 | 401 | [Parameter(Mandatory)] 402 | [string] $FileName 403 | ) 404 | 405 | $xml = [xml]([String]::Format(' 406 | ', $Lang, $Release)) 411 | 412 | Add-Title -Xml $Xml -Listing $Listing 413 | Add-Description -Xml $Xml -Listing $Listing 414 | $icon = Add-Icon -Xml $Xml -Listing $Listing 415 | 416 | $imageNames = @() 417 | $imageNames += $icon 418 | 419 | # Save XML object to file 420 | $filePath = Ensure-PdpFilePath -PdpRootPath $PdpRootPath -Lang $Lang -FileName $FileName 421 | $xml.Save($filePath) 422 | 423 | # Post-process the file to ensure CRLF (sometimes is only LF). 424 | $content = Get-Content -Encoding UTF8 -Path $filePath 425 | $content -join [Environment]::NewLine | Out-File -Force -Encoding utf8 -FilePath $filePath 426 | 427 | return $imageNames 428 | } 429 | 430 | function Ensure-PdpFilePath 431 | { 432 | <# 433 | .SYNOPSIS 434 | Ensures that the containing folder for a PDP file that will be generated exists so that 435 | it can successfully be written. 436 | 437 | .DESCRIPTION 438 | Ensures that the containing folder for a PDP file that will be generated exists so that 439 | it can successfully be written. 440 | 441 | .PARAMETER PdpRootPath 442 | The root / base path that all of the language sub-folders will go for the PDP files. 443 | 444 | .PARAMETER Lang 445 | The language / region code for the PDP (e.g. "en-us") 446 | 447 | .PARAMETER FileName 448 | The name of the PDP file that will be generated. 449 | 450 | .EXAMPLE 451 | Ensure-PdpFilePath -PdpRootPath "C:\PDPs\" -Lang "en-us" -FileName "PDP.xml" 452 | 453 | Ensures that the path c:\PDPs\en-us\ exists, creating any sub-folder along the way as 454 | necessary, and then returns the path "c:\PDPs\en-us\PDP.xml" 455 | 456 | .OUTPUTS 457 | [String] containing the full path to the PDP file. 458 | #> 459 | [CmdletBinding()] 460 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "", Justification="Best description for purpose")] 461 | param( 462 | [Parameter(Mandatory)] 463 | [string] $PdpRootPath, 464 | 465 | [string] $Lang, 466 | 467 | [string] $FileName 468 | ) 469 | 470 | $dropFolder = Join-Path -Path $PdpRootPath -ChildPath $Lang 471 | if (-not (Test-Path -PathType Container -Path $dropFolder)) 472 | { 473 | New-Item -Force -ItemType Directory -Path $dropFolder | Out-Null 474 | } 475 | 476 | return (Join-Path -Path $dropFolder -ChildPath $FileName) 477 | } 478 | 479 | function Show-ImageFileNames 480 | { 481 | <# 482 | .SYNOPSIS 483 | Informs the user what the image filenames are that they need to make available to StoreBroker. 484 | 485 | .DESCRIPTION 486 | Informs the user what the image filenames are that they need to make available to StoreBroker. 487 | 488 | .PARAMETER LangImageNames 489 | A hashtable, indexed by langcode, containing an array of image names that the listing 490 | for that langcode references. 491 | 492 | .PARAMETER Release 493 | The release name that was added to the PDP files. 494 | 495 | .EXAMPLE 496 | Show-ImageFileNames -LangImageNames $langImageNames -Release "1701" 497 | #> 498 | [CmdletBinding()] 499 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="The most common scenario is that there will be multiple images, not a singular image.")] 500 | param( 501 | [Parameter(Mandatory)] 502 | [hashtable] $LangImageNames, 503 | 504 | [Parameter(Mandatory)] 505 | [string] $Release 506 | ) 507 | 508 | # If there are no screenshots, nothing to do here 509 | if ($LangImageNames.Count -eq 0) 510 | { 511 | return 512 | } 513 | 514 | # If there are no images being used at all, then we can also early return 515 | $imageCount = 0 516 | foreach ($lang in $LangImageNames.GetEnumerator()) 517 | { 518 | $imageCount += $lang.Value.Count 519 | } 520 | 521 | if ($imageCount.Count -eq 0) 522 | { 523 | return 524 | } 525 | 526 | Write-Log -Message @( 527 | "You now need to find all of your images and place them here: \$Release\\...", 528 | " where is the path defined in your config file,", 529 | " and is the same langcode for the directory of the PDP file referencing those images.") 530 | 531 | # Quick analysis to help teams out if they need to do anything special with their PDP's 532 | 533 | $langs = $LangImageNames.Keys | ConvertTo-Array 534 | $seenImages = $LangImageNames[$langs[0]] 535 | $imagesDiffer = $false 536 | for ($i = 1; ($i -lt $langs.Count) -and (-not $imagesDiffer); $i++) 537 | { 538 | if (($LangImageNames[$langs[$i]].Count -ne $seenImages.Count)) 539 | { 540 | $imagesDiffer = $true 541 | break 542 | } 543 | 544 | foreach ($image in $LangImageNames[$langs[$i]]) 545 | { 546 | if ($seenImages -notcontains $image) 547 | { 548 | $imagesDiffer = $true 549 | break 550 | } 551 | } 552 | } 553 | 554 | # Now show the user the image filenames 555 | if ($imagesDiffer) 556 | { 557 | Write-Log -Level Warning -Message @( 558 | "It appears that you don't have consistent images across all languages.", 559 | "While StoreBroker supports this scenario, some localization systems may", 560 | "not support this without additional work. Please refer to the FAQ in", 561 | "the documentation for more info on how to best handle this scenario.") 562 | 563 | $output = @() 564 | $output += "The currently referenced image filenames, per langcode, are as follows:" 565 | foreach ($langCode in ($LangImageNames.Keys.GetEnumerator() | Sort-Object)) 566 | { 567 | $output += " * [$langCode]: " + ($LangImageNames.$langCode -join ", ") 568 | } 569 | 570 | Write-Log -Message $output 571 | } 572 | else 573 | { 574 | Write-Log -Message @( 575 | "Every language that has a PDP references the following images:", 576 | "`t$($seenImages -join `"`n`t`")") 577 | } 578 | } 579 | 580 | # function Main is invoked at the bottom of the file 581 | function Main 582 | { 583 | [CmdletBinding()] 584 | param() 585 | 586 | if ($null -eq (Get-Module StoreBroker)) 587 | { 588 | $message = "The StoreBroker module is not available in this PowerShell session. Please import the module, authenticate correctly using Set-StoreBrokerAuthentication, and try again." 589 | throw $message 590 | } 591 | 592 | $sub = $Submission 593 | if ($null -eq $sub) 594 | { 595 | if ([String]::IsNullOrEmpty($SubmissionId)) 596 | { 597 | $iap = Get-InAppProduct -IapId $IapId 598 | $SubmissionId = $iap.lastPublishedInAppProductSubmission.id 599 | if ([String]::IsNullOrEmpty($SubmissionId)) 600 | { 601 | $SubmissionId = $iap.pendingInAppProductSubmission.id 602 | Write-Log -Message "No published submission exists for this In-App Product. Using the current pending submission." -Level Warning 603 | } 604 | } 605 | 606 | $sub = Get-InAppProductSubmission -IapId $IapId -SubmissionId $SubmissionId 607 | } 608 | 609 | $langImageNames = @{} 610 | $langs = ($sub.listings | Get-Member -type NoteProperty) 611 | $pdpsGenerated = 0 612 | $langs | 613 | ForEach-Object { 614 | $lang = $_.Name 615 | Write-Log -Message "Creating PDP for $lang" -Level Verbose 616 | Write-Progress -Activity "Generating PDP" -Status $lang -PercentComplete $(($pdpsGenerated / $langs.Count) * 100) 617 | try 618 | { 619 | $imageNames = ConvertFrom-Listing -Listing ($sub.listings.$lang) -Lang $lang -Release $Release -PdpRootPath $OutPath -FileName $PdpFileName 620 | $langImageNames[$lang] = $imageNames 621 | $pdpsGenerated++ 622 | } 623 | catch 624 | { 625 | Write-Log -Message "Error creating [$lang] PDP:" -Exception $_ -Level Error 626 | throw 627 | } 628 | } 629 | 630 | if ($pdpsGenerated -gt 0) 631 | { 632 | Write-Log -Message "PDP's have been created here: $OutPath" 633 | Show-ImageFileNames -LangImageNames $langImageNames -Release $Release 634 | } 635 | else 636 | { 637 | Write-Log -Level Warning -Message @( 638 | "No PDPs were generated.", 639 | "Please verify that this existing In-App Product has one or more language listings that this extension can convert,", 640 | "otherwise you can start fresh using the sample PDP\InAppProductDescription.xml as a starting point.") 641 | } 642 | } 643 | 644 | 645 | 646 | 647 | # Script body 648 | $OutPath = Resolve-UnverifiedPath -Path $OutPath 649 | 650 | # function Main invocation 651 | Main 652 | --------------------------------------------------------------------------------