diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 52b48d0815..06cc51c42f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -596,6 +596,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -700,7 +701,8 @@ namespace Emby.Server.Implementations SetStaticProperties(); - ((SqliteItemRepository)Resolve()).Initialize(); + var userDataRepo = (SqliteUserDataRepository)Resolve(); + ((SqliteItemRepository)Resolve()).Initialize(userDataRepo, Resolve()); FindParts(); } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 74b8ffc18d..ca5cd6fdd5 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Data /// /// Opens the connection to the database /// - public void Initialize() + public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager) { const string CreateMediaStreamsTableCommand = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; @@ -324,6 +324,8 @@ namespace Emby.Server.Implementations.Data connection.RunQueries(postQueries); } + + userDataRepo.Initialize(userManager, WriteLock, WriteConnection); } private static readonly string[] _retriveItemColumns = diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 6ee6230fc6..b99b74ef81 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Threading; +using Jellyfin.Data.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 8cf144b17c..17913959ee 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using Jellyfin.Data.Enums; namespace Jellyfin.Data.Entities @@ -121,7 +122,8 @@ namespace Jellyfin.Data.Entities /// [Key] [Required] - public Guid Id { get; protected set; } + [JsonIgnore] + public Guid Id { get; set; } /// /// Required, Max length = 255 @@ -129,6 +131,7 @@ namespace Jellyfin.Data.Entities [Required] [MaxLength(255)] [StringLength(255)] + [JsonPropertyName("Name")] public string Username { get; set; } /// diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 7e13146902..35003c1ae3 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -16,10 +16,6 @@ namespace Jellyfin.Server.Implementations public partial class JellyfinDb : DbContext { public virtual DbSet ActivityLogs { get; set; } - public virtual DbSet Groups { get; set; } - public virtual DbSet Permissions { get; set; } - public virtual DbSet Preferences { get; set; } - public virtual DbSet ProviderMappings { get; set; } public virtual DbSet Users { get; set; } /*public virtual DbSet Artwork { get; set; } public virtual DbSet Books { get; set; } @@ -34,6 +30,7 @@ namespace Jellyfin.Server.Implementations public virtual DbSet Episodes { get; set; } public virtual DbSet EpisodeMetadata { get; set; } public virtual DbSet Genres { get; set; } + public virtual DbSet Groups { get; set; } public virtual DbSet Libraries { get; set; } public virtual DbSet LibraryItems { get; set; } public virtual DbSet LibraryRoot { get; set; } @@ -46,10 +43,13 @@ namespace Jellyfin.Server.Implementations public virtual DbSet MovieMetadata { get; set; } public virtual DbSet MusicAlbums { get; set; } public virtual DbSet MusicAlbumMetadata { get; set; } + public virtual DbSet Permissions { get; set; } public virtual DbSet People { get; set; } public virtual DbSet PersonRoles { get; set; } public virtual DbSet Photo { get; set; } public virtual DbSet PhotoMetadata { get; set; } + public virtual DbSet Preferences { get; set; } + public virtual DbSet ProviderMappings { get; set; } public virtual DbSet Ratings { get; set; } /// @@ -63,7 +63,7 @@ namespace Jellyfin.Server.Implementations public virtual DbSet Series { get; set; } public virtual DbSet SeriesMetadata { get; set; } public virtual DbSet Tracks { get; set; } - public virtual DbSet TrackMetadata { get; set; } */ + public virtual DbSet TrackMetadata { get; set; }*/ /// /// Gets or sets the default connection string. diff --git a/Jellyfin.Server.Implementations/User/UserManager.cs b/Jellyfin.Server.Implementations/User/UserManager.cs index 1ed11cfcbb..73905ff70b 100644 --- a/Jellyfin.Server.Implementations/User/UserManager.cs +++ b/Jellyfin.Server.Implementations/User/UserManager.cs @@ -306,6 +306,28 @@ namespace Jellyfin.Server.Implementations.User }; } + public PublicUserDto GetPublicUserDto(Data.Entities.User user, string remoteEndPoint = null) + { + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user); + bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user)); + + bool hasPassword = user.EnableLocalPassword && + !string.IsNullOrEmpty(remoteEndPoint) && + _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : hasConfiguredPassword; + + return new PublicUserDto + { + Name = user.Username, + HasPassword = hasPassword, + HasConfiguredPassword = hasConfiguredPassword + }; + } + public async Task AuthenticateUser( string username, string password, diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 4194070aa9..51b2a8cc62 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -44,6 +44,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index ca17482820..fb793a7cf1 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -17,7 +17,9 @@ namespace Jellyfin.Server.Migrations private static readonly Type[] _migrationTypes = { typeof(Routines.DisableTranscodingThrottling), - typeof(Routines.CreateUserLoggingConfigFile) + typeof(Routines.CreateUserLoggingConfigFile), + typeof(Routines.MigrateActivityLogDb), + typeof(Routines.MigrateUserDb) }; /// diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index 9f1f5b92eb..fe7ef01eef 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -5,8 +5,8 @@ using System.IO; using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; using Jellyfin.Server.Implementations; +using MediaBrowser.Controller; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; @@ -16,20 +16,31 @@ namespace Jellyfin.Server.Migrations.Routines { private const string DbFilename = "activitylog.db"; + private readonly ILogger _logger; + private readonly JellyfinDbProvider _provider; + private readonly IServerApplicationPaths _paths; + + public MigrateActivityLogDb(ILogger logger, IServerApplicationPaths paths, JellyfinDbProvider provider) + { + _logger = logger; + _provider = provider; + _paths = paths; + } + public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978"); public string Name => "MigrateActivityLogDatabase"; - public void Perform(CoreAppHost host, ILogger logger) + public void Perform() { - var dataPath = host.ServerConfigurationManager.ApplicationPaths.DataPath; + var dataPath = _paths.DataPath; using (var connection = SQLite3.Open( Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null)) { - logger.LogInformation("Migrating the database may take a while, do not stop Jellyfin."); - using var dbContext = host.ServiceProvider.GetService(); + _logger.LogInformation("Migrating the database may take a while, do not stop Jellyfin."); + using var dbContext = _provider.CreateContext(); var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id ASC"); @@ -75,7 +86,7 @@ namespace Jellyfin.Server.Migrations.Routines } catch (IOException e) { - logger.LogError(e, "Error renaming legacy activity log database to 'activitylog.db.old'"); + _logger.LogError(e, "Error renaming legacy activity log database to 'activitylog.db.old'"); } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 23d2299cdb..b19d7f7fca 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -1,8 +1,144 @@ #pragma warning disable CS1591 +using System; +using System.Collections.Generic; +using System.IO; +using Emby.Server.Implementations.Data; +using Emby.Server.Implementations.Serialization; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Server.Implementations; +using MediaBrowser.Controller; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Users; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using SQLitePCL.pretty; + namespace Jellyfin.Server.Migrations.Routines { - public class MigrateUserDb + public class MigrateUserDb : IMigrationRoutine { + private readonly ILogger _logger; + + private readonly IServerApplicationPaths _paths; + + private readonly JellyfinDbProvider _provider; + + private readonly MyXmlSerializer _xmlSerializer; + + public MigrateUserDb(ILogger logger, IServerApplicationPaths paths, JellyfinDbProvider provider, MyXmlSerializer xmlSerializer) + { + _logger = logger; + _paths = paths; + _provider = provider; + _xmlSerializer = xmlSerializer; + } + + public Guid Id => Guid.Parse("5C4B82A2-F053-4009-BD05-B6FCAD82F14C"); + + public string Name => "MigrateUserDb"; + + public void Perform() + { + var dataPath = _paths.DataPath; + _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin."); + + using (var connection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null)) + { + using var dbContext = _provider.CreateContext(); + + var queryResult = connection.Query("SELECT * FROM LocalUsersv2"); + + dbContext.RemoveRange(dbContext.Users); + dbContext.SaveChanges(); + + foreach (var entry in queryResult) + { + var json = JsonConvert.DeserializeObject>(entry[2].ToString()); + var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, json["Name"]); + + var config = (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml")); + var policy = (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml")); + + var user = new User( + json["Name"], + false, + policy.AuthenticatioIsnProviderId, + policy.InvalidLoginAttemptCount, + config.SubtitleMode, + config.PlayDefaultAudioTrack) + { + Id = entry[1].ReadGuidFromBlob(), + InternalId = entry[0].ToInt64(), + MaxParentalAgeRating = policy.MaxParentalRating, + EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess, + RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit, + AuthenticationProviderId = policy.AuthenticatioIsnProviderId, + PasswordResetProviderId = policy.PasswordResetProviderId, + InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount, + LoginAttemptsBeforeLockout = policy.LoginAttemptsBeforeLockout == -1 ? null : new int?(policy.LoginAttemptsBeforeLockout), + SubtitleMode = config.SubtitleMode, + HidePlayedInLatest = config.HidePlayedInLatest, + EnableLocalPassword = config.EnableLocalPassword, + PlayDefaultAudioTrack = config.PlayDefaultAudioTrack, + DisplayCollectionsView = config.DisplayCollectionsView, + DisplayMissingEpisodes = config.DisplayMissingEpisodes, + AudioLanguagePreference = config.AudioLanguagePreference, + RememberAudioSelections = config.RememberAudioSelections, + EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay, + RememberSubtitleSelections = config.RememberSubtitleSelections, + SubtitleLanguagePreference = config.SubtitleLanguagePreference, + }; + + user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator); + user.SetPermission(PermissionKind.IsHidden, policy.IsHidden); + user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled); + user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl); + user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess); + user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement); + user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess); + user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback); + user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding); + user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding); + user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion); + user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading); + user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding); + user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion); + user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels); + user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices); + user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders); + user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers); + user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing); + user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding); + user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing); + foreach (var policyAccessSchedule in policy.AccessSchedules) + { + user.AccessSchedules.Add(policyAccessSchedule); + } + + user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags); + user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels); + user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices); + user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders); + user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); + user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); + user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); + user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); + user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); + } + + dbContext.SaveChanges(); + } + + try + { + File.Move(Path.Combine(dataPath, "users.db"), Path.Combine(dataPath, "users.db" + ".old")); + } + catch (IOException e) + { + _logger.LogError(e, "Error renaming legacy user database to 'users.db.old'"); + } + } } } diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index f9976122c8..5bdf1618bc 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -884,9 +884,10 @@ namespace MediaBrowser.Api.Images // Handle image/png; charset=utf-8 mimeType = mimeType.Split(';').FirstOrDefault(); + var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); await _providerManager - .SaveImage(user, memoryStream, mimeType, Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, _imageProcessor.GetImageCacheTag(user))) + .SaveImage(user, memoryStream, mimeType, Path.Combine(userDataPath, _imageProcessor.GetImageCacheTag(user))) .ConfigureAwait(false); await _userManager.UpdateUserAsync(user); } diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index 7cee23abee..8d4450c1ae 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -276,12 +276,12 @@ namespace MediaBrowser.Api { var result = _userManager .Users - .Where(item => !item.Policy.IsDisabled); + .Where(item => !item.HasPermission(PermissionKind.IsDisabled)); if (ServerConfigurationManager.Configuration.IsStartupWizardCompleted) { var deviceId = _authContext.GetAuthorizationInfo(Request).DeviceId; - result = result.Where(item => !item.Policy.IsHidden); + result = result.Where(item => !item.HasPermission(PermissionKind.IsHidden)); if (!string.IsNullOrWhiteSpace(deviceId)) { @@ -290,12 +290,12 @@ namespace MediaBrowser.Api if (!_networkManager.IsInLocalNetwork(Request.RemoteIp)) { - result = result.Where(i => i.Policy.EnableRemoteAccess); + result = result.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess)); } } return ToOptimizedResult(result - .OrderBy(u => u.Name) + .OrderBy(u => u.Username) .Select(i => _userManager.GetPublicUserDto(i, Request.RemoteIp)) .ToArray() ); diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 77f09252e3..6685f5306e 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -144,7 +144,7 @@ namespace MediaBrowser.Controller.Library /// /// Gets the user public dto. /// - /// Ther user.\ + /// The user.\ /// The remote end point. /// A public UserDto, aka a UserDto stripped of personal data. PublicUserDto GetPublicUserDto(User user, string remoteEndPoint = null); diff --git a/MediaBrowser.sln b/MediaBrowser.sln index d5bc161866..210e2b6448 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -61,11 +61,13 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Naming.Tests", "tests\Jellyfin.Naming.Tests\Jellyfin.Naming.Tests.csproj", "{3998657B-1CCC-49DD-A19F-275DC8495F57}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Api.Tests", "tests\Jellyfin.Api.Tests\Jellyfin.Api.Tests.csproj", "{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}" -("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations.Tests", "tests\Jellyfin.Server.Implementations.Tests\Jellyfin.Server.Implementations.Tests.csproj", "{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api.Tests", "tests\MediaBrowser.Api.Tests\MediaBrowser.Api.Tests.csproj", "{7C93C84F-105C-48E5-A878-406FA0A5B296}" @@ -75,8 +77,7 @@ Global Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection - GlobalSectEndProject - Projection(ProjectConfigurationPlatforms) = postSolution + GlobalSection(ProjectConfigurationPlatforms) = postSolution {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -185,26 +186,18 @@ Global {F03299F2-469F-40EF-A655-3766F97A5702}.Debug|Any CPU.Build.0 = Debug|Any CPU {F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.ActiveCfg = Release|Any CPU {F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.Build.0 = Release|Any CPU - {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.ActiveCfg = Release|Any CPU - {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.Build.0 = Release|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU + {7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} EndGlobalSection