diff --git a/src/Bootstrap/dist/css/bootstrap-theme.css b/src/Bootstrap/dist/css/bootstrap-theme.css index 84c00273cc..3c2a5e6c7d 100644 --- a/src/Bootstrap/dist/css/bootstrap-theme.css +++ b/src/Bootstrap/dist/css/bootstrap-theme.css @@ -624,6 +624,10 @@ img.reserved-indicator-icon { display: inline-block; width: 100%; } +.page-account-settings .expired-certificate { + font-weight: bold; + color: #a94442; +} .page-api-keys .example-commands { padding: 8px 10px; font-family: Consolas, Menlo, Monaco, "Courier New", monospace; @@ -824,6 +828,13 @@ img.reserved-indicator-icon { .page-package-details .package-details-info .ms-Icon-ul li { margin-bottom: 15px; } +.page-package-details .package-details-info .ms-Icon-ul img.icon { + position: absolute; + left: -24px; + width: 16px; + height: 16px; + margin-top: 3px; +} .page-package-details .owner-list li { margin-bottom: 8px; } @@ -1201,6 +1212,10 @@ img.reserved-indicator-icon { .page-manage-packages .inner-table { margin-bottom: 0; } +.page-manage-packages .required-signer { + width: 100%; + max-width: 90%; +} .page-delete-package h1 { margin-bottom: 0; } diff --git a/src/Bootstrap/less/theme/page-account-settings.less b/src/Bootstrap/less/theme/page-account-settings.less index b0e62de003..6b273ef260 100644 --- a/src/Bootstrap/less/theme/page-account-settings.less +++ b/src/Bootstrap/less/theme/page-account-settings.less @@ -49,4 +49,9 @@ display: inline-block; width: 100%; } + + .expired-certificate { + color: @state-danger-text; + font-weight: bold; + } } diff --git a/src/Bootstrap/less/theme/page-display-package.less b/src/Bootstrap/less/theme/page-display-package.less index 154d829ca0..a16c4b0a09 100644 --- a/src/Bootstrap/less/theme/page-display-package.less +++ b/src/Bootstrap/less/theme/page-display-package.less @@ -44,6 +44,13 @@ li { margin-bottom: 15px; } + img.icon { + position: absolute; + left: -24px; + width: 16px; + height: 16px; + margin-top: 3px; + } } } diff --git a/src/Bootstrap/less/theme/page-manage-packages.less b/src/Bootstrap/less/theme/page-manage-packages.less index 50ad961133..91ec2245a0 100644 --- a/src/Bootstrap/less/theme/page-manage-packages.less +++ b/src/Bootstrap/less/theme/page-manage-packages.less @@ -44,7 +44,7 @@ .panel { margin-top: 5px; } - + .manage-package-listing { .ms-Icon { position: relative; @@ -55,4 +55,9 @@ .inner-table { margin-bottom: 0px; } + + .required-signer { + width: 100%; + max-width: 90%; + } } diff --git a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs index 7591f727d2..2122b8b6a7 100644 --- a/src/NuGetGallery.Core/Entities/IEntitiesContext.cs +++ b/src/NuGetGallery.Core/Entities/IEntitiesContext.cs @@ -18,6 +18,7 @@ public interface IEntitiesContext IDbSet UserSecurityPolicies { get; set; } IDbSet ReservedNamespaces { get; set; } IDbSet UserCertificates { get; set; } + IDbSet SymbolPackages { get; set; } Task SaveChangesAsync(); [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Set", Justification="This is to match the EF terminology.")] diff --git a/src/NuGetGallery.Core/Entities/Package.cs b/src/NuGetGallery.Core/Entities/Package.cs index 007359fc25..9fef8138a2 100644 --- a/src/NuGetGallery.Core/Entities/Package.cs +++ b/src/NuGetGallery.Core/Entities/Package.cs @@ -21,6 +21,7 @@ public Package() PackageHistories = new HashSet(); PackageTypes = new HashSet(); SupportedFrameworks = new HashSet(); + SymbolPackages = new HashSet(); Listed = true; } #pragma warning restore 618 @@ -123,6 +124,13 @@ public Package() /// public string RepositoryUrl { get; set; } + + /// + /// Has a max length of 100. Is not indexed and not used for searches. Db column is nvarchar(100). + /// + [StringLength(100)] + public string RepositoryType { get; set; } + /// /// Nullable flag stored in the database. Callers should use the HasReadMe property instead. /// diff --git a/src/NuGetGallery.Core/Entities/SymbolPackage.cs b/src/NuGetGallery.Core/Entities/SymbolPackage.cs index 5cb194d192..fe03f533bc 100644 --- a/src/NuGetGallery.Core/Entities/SymbolPackage.cs +++ b/src/NuGetGallery.Core/Entities/SymbolPackage.cs @@ -8,7 +8,7 @@ namespace NuGetGallery { public class SymbolPackage - : IEntity + : IEntity, IEquatable { public int Key { get; set; } @@ -19,7 +19,6 @@ public class SymbolPackage /// /// Timestamp when this symbol was created. /// - [DatabaseGenerated(DatabaseGeneratedOption.Computed)] public DateTime Created { get; set; } /// @@ -51,5 +50,51 @@ public class SymbolPackage /// Used for optimistic concurrency when updating the symbols. /// public byte[] RowVersion { get; set; } + + public bool Equals(SymbolPackage other) + { + return other.Key == Key; + } + + public override int GetHashCode() + { + return Key.GetHashCode(); + } + + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + + SymbolPackage sp = obj as SymbolPackage; + if (sp == null) + { + return false; + } + + return Equals(sp); + } + + public static bool operator ==(SymbolPackage sp1, SymbolPackage sp2) + { + if (((object)sp1) == null || ((object)sp2) == null) + { + return Equals(sp1, sp2); + } + + return sp1.Equals(sp2); + } + + public static bool operator !=(SymbolPackage sp1, SymbolPackage sp2) + { + if (((object)sp1) == null || ((object)sp2) == null) + { + return !Equals(sp1, sp2); + } + + return !sp1.Equals(sp2); + } } } \ No newline at end of file diff --git a/src/NuGetGallery.Core/Packaging/PackageMetadata.cs b/src/NuGetGallery.Core/Packaging/PackageMetadata.cs index cda9fbe26f..1e0284e9c6 100644 --- a/src/NuGetGallery.Core/Packaging/PackageMetadata.cs +++ b/src/NuGetGallery.Core/Packaging/PackageMetadata.cs @@ -50,7 +50,8 @@ public PackageMetadata( IEnumerable dependencyGroups, IEnumerable frameworkGroups, IEnumerable packageTypes, - NuGetVersion minClientVersion) + NuGetVersion minClientVersion, + RepositoryMetadata repositoryMetadata) { _metadata = new Dictionary(metadata, StringComparer.OrdinalIgnoreCase); _dependencyGroups = dependencyGroups.ToList().AsReadOnly(); @@ -59,6 +60,13 @@ public PackageMetadata( SetPropertiesFromMetadata(); MinClientVersion = minClientVersion; + + if (repositoryMetadata != null) + { + Uri.TryCreate(repositoryMetadata.Url, UriKind.Absolute, out var repoUrl); + RepositoryUrl = repoUrl; + RepositoryType = repositoryMetadata.Type; + } } private void SetPropertiesFromMetadata() @@ -100,6 +108,8 @@ private void SetPropertiesFromMetadata() public Uri IconUrl { get; private set; } public Uri ProjectUrl { get; private set; } + public Uri RepositoryUrl { get; private set; } + public string RepositoryType { get; private set; } public Uri LicenseUrl { get; private set; } public string Copyright { get; private set; } public string Description { get; private set; } @@ -180,7 +190,7 @@ private Uri GetValue(string key, Uri alternateValue) /// Whether or not to be strict when reading the . This should be true /// on initial ingestion but false when a package that has already been accepted is being processed. /// - /// We default to use a strict version-check on dependency groups. + /// We default to use a strict version-check on dependency groups. /// When an invalid dependency version range is detected, a will be thrown. /// public static PackageMetadata FromNuspecReader(NuspecReader nuspecReader, bool strict) @@ -233,8 +243,8 @@ public static PackageMetadata FromNuspecReader(NuspecReader nuspecReader, bool s nuspecReader.GetDependencyGroups(useStrictVersionCheck: strict), nuspecReader.GetFrameworkReferenceGroups(), nuspecReader.GetPackageTypes(), - nuspecReader.GetMinClientVersion() - ); + nuspecReader.GetMinClientVersion(), + nuspecReader.GetRepositoryMetadata()); } private class StrictNuspecReader : NuspecReader @@ -251,4 +261,4 @@ public ILookup GetMetadataLookup() } } } -} \ No newline at end of file +} diff --git a/src/NuGetGallery.Core/Services/CloudBlobCoreFileStorageService.cs b/src/NuGetGallery.Core/Services/CloudBlobCoreFileStorageService.cs index 4b8b61606f..55831779b0 100644 --- a/src/NuGetGallery.Core/Services/CloudBlobCoreFileStorageService.cs +++ b/src/NuGetGallery.Core/Services/CloudBlobCoreFileStorageService.cs @@ -30,7 +30,9 @@ public class CloudBlobCoreFileStorageService : ICoreFileStorageService private static readonly HashSet KnownPublicFolders = new HashSet { CoreConstants.PackagesFolderName, CoreConstants.PackageBackupsFolderName, - CoreConstants.DownloadsFolderName + CoreConstants.DownloadsFolderName, + CoreConstants.SymbolPackagesFolderName, + CoreConstants.SymbolPackageBackupsFolderName }; private static readonly HashSet KnownPrivateFolders = new HashSet { @@ -537,6 +539,8 @@ private static string GetContentType(string folderName) case CoreConstants.PackageBackupsFolderName: case CoreConstants.UploadsFolderName: case CoreConstants.ValidationFolderName: + case CoreConstants.SymbolPackagesFolderName: + case CoreConstants.SymbolPackageBackupsFolderName: return CoreConstants.PackageContentType; case CoreConstants.DownloadsFolderName: diff --git a/src/NuGetGallery.Core/Services/CoreMessageService.cs b/src/NuGetGallery.Core/Services/CoreMessageService.cs index a0581375a5..cf7b86bb65 100644 --- a/src/NuGetGallery.Core/Services/CoreMessageService.cs +++ b/src/NuGetGallery.Core/Services/CoreMessageService.cs @@ -2,10 +2,13 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.ObjectModel; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Mail; using System.Text; +using System.Threading.Tasks; using AnglicanGeek.MarkdownMailer; using NuGet.Services.Validation; using NuGet.Services.Validation.Issues; @@ -14,9 +17,11 @@ namespace NuGetGallery.Services { public class CoreMessageService : ICoreMessageService { - protected CoreMessageService() - { - } + private static readonly ReadOnlyCollection RetryDelays = Array.AsReadOnly(new[] { + TimeSpan.FromSeconds(0.1), + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(10) + }); public CoreMessageService(IMailSender mailSender, ICoreMessageServiceConfiguration coreConfiguration) { @@ -27,7 +32,7 @@ public CoreMessageService(IMailSender mailSender, ICoreMessageServiceConfigurati public IMailSender MailSender { get; protected set; } public ICoreMessageServiceConfiguration CoreConfiguration { get; protected set; } - public void SendPackageAddedNotice(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl) + public async Task SendPackageAddedNoticeAsync(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl) { string subject = $"[{CoreConfiguration.GalleryOwner.DisplayName}] Package published - {package.PackageRegistration.Id} {package.Version}"; string body = $@"The package [{package.PackageRegistration.Id} {package.Version}]({packageUrl}) was recently published on {CoreConfiguration.GalleryOwner.DisplayName} by {package.User.Username}. If this was not intended, please [contact support]({packageSupportUrl}). @@ -48,12 +53,12 @@ [change your email notification settings]({emailSettingsUrl}). if (mailMessage.To.Any()) { - SendMessage(mailMessage, copySender: false); + await SendMessageAsync(mailMessage); } } } - public void SendPackageValidationFailedNotice(Package package, PackageValidationSet validationSet, string packageUrl, string packageSupportUrl, string announcementsUrl, string twitterUrl) + public async Task SendPackageValidationFailedNoticeAsync(Package package, PackageValidationSet validationSet, string packageUrl, string packageSupportUrl, string announcementsUrl, string twitterUrl) { var validationIssues = validationSet.GetValidationIssues(); @@ -94,7 +99,7 @@ public void SendPackageValidationFailedNotice(Package package, PackageValidation if (mailMessage.To.Any()) { - SendMessage(mailMessage, copySender: false); + await SendMessageAsync(mailMessage); } } } @@ -130,7 +135,7 @@ private static string ParseValidationIssue(ValidationIssue validationIssue, stri } } - public void SendValidationTakingTooLongNotice(Package package, string packageUrl) + public async Task SendValidationTakingTooLongNoticeAsync(Package package, string packageUrl) { string subject = "[{0}] Package validation taking longer than expected - {1} {2}"; string body = "It is taking longer than expected for your package [{1} {2}]({3}) to get published.\n\n" + @@ -162,7 +167,7 @@ public void SendValidationTakingTooLongNotice(Package package, string packageUrl if (mailMessage.To.Any()) { - SendMessage(mailMessage, copySender: false); + await SendMessageAsync(mailMessage); } } } @@ -192,30 +197,54 @@ protected static void AddOwnersSubscribedToPackagePushedNotification(PackageRegi } } - protected void SendMessage(MailMessage mailMessage) + protected virtual async Task SendMessageAsync(MailMessage mailMessage) { - SendMessage(mailMessage, copySender: false); + int attempt = 0; + bool success = false; + while (!success) + { + try + { + await AttemptSendMessageAsync(mailMessage, attempt + 1); + success = true; + } + catch (SmtpException) + { + if (attempt < RetryDelays.Count) + { + await Task.Delay(RetryDelays[attempt]); + attempt++; + } + else + { + throw; + } + } + } } - virtual protected void SendMessage(MailMessage mailMessage, bool copySender) + protected virtual Task AttemptSendMessageAsync(MailMessage mailMessage, int attemptNumber) { + // AnglicanGeek.MarkdownMailer doesn't have an async overload MailSender.Send(mailMessage); - if (copySender) + return Task.CompletedTask; + } + + protected async Task SendMessageToSenderAsync(MailMessage mailMessage) + { + using (var senderCopy = new MailMessage( + CoreConfiguration.GalleryOwner, + mailMessage.ReplyToList.First())) { - var senderCopy = new MailMessage( - CoreConfiguration.GalleryOwner, - mailMessage.ReplyToList.First()) - { - Subject = mailMessage.Subject + " [Sender Copy]", - Body = string.Format( - CultureInfo.CurrentCulture, - "You sent the following message via {0}: {1}{1}{2}", - CoreConfiguration.GalleryOwner.DisplayName, - Environment.NewLine, - mailMessage.Body), - }; + senderCopy.Subject = mailMessage.Subject + " [Sender Copy]"; + senderCopy.Body = string.Format( + CultureInfo.CurrentCulture, + "You sent the following message via {0}: {1}{1}{2}", + CoreConfiguration.GalleryOwner.DisplayName, + Environment.NewLine, + mailMessage.Body); senderCopy.ReplyToList.Add(mailMessage.ReplyToList.First()); - MailSender.Send(senderCopy); + await SendMessageAsync(senderCopy); } } } diff --git a/src/NuGetGallery.Core/Services/CorePackageFileService.cs b/src/NuGetGallery.Core/Services/CorePackageFileService.cs index b1a7df26b7..c7e7c3c945 100644 --- a/src/NuGetGallery.Core/Services/CorePackageFileService.cs +++ b/src/NuGetGallery.Core/Services/CorePackageFileService.cs @@ -22,6 +22,11 @@ public CorePackageFileService(ICoreFileStorageService fileStorageService, IFileM } public Task SavePackageFileAsync(Package package, Stream packageFile) + { + return SavePackageFileAsync(package, packageFile, overwrite: false); + } + + public Task SavePackageFileAsync(Package package, Stream packageFile, bool overwrite) { if (packageFile == null) { @@ -29,7 +34,7 @@ public Task SavePackageFileAsync(Package package, Stream packageFile) } var fileName = BuildFileName(package, _metadata.FileSavePathTemplate, _metadata.FileExtension); - return _fileStorageService.SaveFileAsync(_metadata.FileFolderName, fileName, packageFile, overwrite: false); + return _fileStorageService.SaveFileAsync(_metadata.FileFolderName, fileName, packageFile, overwrite); } public Task DownloadPackageFileAsync(Package package) diff --git a/src/NuGetGallery.Core/Services/CoreSymbolPackageService.cs b/src/NuGetGallery.Core/Services/CoreSymbolPackageService.cs index fa686f8867..7b5025e286 100644 --- a/src/NuGetGallery.Core/Services/CoreSymbolPackageService.cs +++ b/src/NuGetGallery.Core/Services/CoreSymbolPackageService.cs @@ -9,8 +9,8 @@ namespace NuGetGallery { public class CoreSymbolPackageService : ICoreSymbolPackageService { - private readonly IEntityRepository _symbolPackageRepository; - private readonly ICorePackageService _corePackageService; + protected readonly IEntityRepository _symbolPackageRepository; + protected readonly ICorePackageService _corePackageService; public CoreSymbolPackageService( IEntityRepository symbolPackageRepository, diff --git a/src/NuGetGallery.Core/Services/ICoreMessageService.cs b/src/NuGetGallery.Core/Services/ICoreMessageService.cs index 2f81dafcad..25dfa773b9 100644 --- a/src/NuGetGallery.Core/Services/ICoreMessageService.cs +++ b/src/NuGetGallery.Core/Services/ICoreMessageService.cs @@ -2,13 +2,14 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using NuGet.Services.Validation; +using System.Threading.Tasks; namespace NuGetGallery.Services { public interface ICoreMessageService { - void SendPackageAddedNotice(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl); - void SendPackageValidationFailedNotice(Package package, PackageValidationSet validationSet, string packageUrl, string packageSupportUrl, string announcementsUrl, string twitterUrl); - void SendValidationTakingTooLongNotice(Package package, string packageUrl); + Task SendPackageAddedNoticeAsync(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl); + Task SendPackageValidationFailedNoticeAsync(Package package, PackageValidationSet validationSet, string packageUrl, string packageSupportUrl, string announcementsUrl, string twitterUrl); + Task SendValidationTakingTooLongNoticeAsync(Package package, string packageUrl); } } \ No newline at end of file diff --git a/src/NuGetGallery.Core/Services/ICorePackageFileService.cs b/src/NuGetGallery.Core/Services/ICorePackageFileService.cs index f0efb7910f..b155a283ac 100644 --- a/src/NuGetGallery.Core/Services/ICorePackageFileService.cs +++ b/src/NuGetGallery.Core/Services/ICorePackageFileService.cs @@ -14,6 +14,11 @@ public interface ICorePackageFileService /// Task SavePackageFileAsync(Package package, Stream packageFile); + /// + /// Saves the contents of the package to the public container for available packages, will overwrite file if needed. + /// + Task SavePackageFileAsync(Package package, Stream packageFile, bool overwrite); + /// /// Downloads the package from the file storage and reads it into a stream. /// diff --git a/src/NuGetGallery/App_Code/ViewHelpers.cshtml b/src/NuGetGallery/App_Code/ViewHelpers.cshtml index a2c1213679..bcca6db8e3 100644 --- a/src/NuGetGallery/App_Code/ViewHelpers.cshtml +++ b/src/NuGetGallery/App_Code/ViewHelpers.cshtml @@ -47,6 +47,16 @@ } +@helper Alert(string htmlContent, string subclass, string icon, bool isAlertRole = false) +{ + @Alert(s => new HelperResult(t => t.Write(htmlContent)), subclass, icon, isAlertRole); +} + +@helper AlertInfo(string htmlContent, bool isAlertRole = false) +{ + @Alert(htmlContent, "info", "Info", isAlertRole) +} + @helper AlertInfo(Func htmlContent, bool isAlertRole = false) { @Alert(htmlContent, "info", "Info", isAlertRole) diff --git a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs index 4b4f01aab0..166a335837 100644 --- a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs +++ b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs @@ -35,6 +35,7 @@ using NuGetGallery.Infrastructure.Authentication; using NuGetGallery.Infrastructure.Lucene; using NuGetGallery.Security; +using NuGetGallery.Services; using SecretReaderFactory = NuGetGallery.Configuration.SecretReader.SecretReaderFactory; namespace NuGetGallery @@ -190,6 +191,11 @@ protected override void Load(ContainerBuilder builder) .As>() .InstancePerLifetimeScope(); + builder.RegisterType>() + .AsSelf() + .As>() + .InstancePerLifetimeScope(); + builder.RegisterType() .AsSelf() .As() @@ -259,11 +265,21 @@ protected override void Load(ContainerBuilder builder) .As() .InstancePerLifetimeScope(); + builder.RegisterType() + .AsSelf() + .As() + .InstancePerLifetimeScope(); + builder.RegisterType() .AsSelf() .As() .InstancePerLifetimeScope(); + builder.RegisterType() + .AsSelf() + .As() + .InstancePerLifetimeScope(); + builder.RegisterType() .AsSelf() .As() @@ -341,7 +357,7 @@ protected override void Load(ContainerBuilder builder) .As() .InstancePerLifetimeScope(); - builder.RegisterType() + builder.RegisterType() .AsSelf() .As() .InstancePerLifetimeScope(); diff --git a/src/NuGetGallery/App_Start/Routes.cs b/src/NuGetGallery/App_Start/Routes.cs index 4c4eb11e21..f027dbc3b8 100644 --- a/src/NuGetGallery/App_Start/Routes.cs +++ b/src/NuGetGallery/App_Start/Routes.cs @@ -662,6 +662,12 @@ public static void RegisterApiV2Routes(RouteCollection routes) defaults: new { controller = "Api", action = "PushPackageApi" }, constraints: new { httpMethod = new HttpMethodConstraint("PUT") }); + routes.MapRoute( + "v2" + RouteName.PushSymbolPackageApi, + "api/v2/symbolpackage", + defaults: new { controller = "Api", action = "PushSymbolPackageApi" }, + constraints: new { httpMethod = new HttpMethodConstraint("PUT") }); + routes.MapRoute( "v2" + RouteName.DeletePackageApi, "api/v2/package/{id}/{version}", diff --git a/src/NuGetGallery/App_Start/StorageDependent.cs b/src/NuGetGallery/App_Start/StorageDependent.cs index c9e31a3ebd..366d156319 100644 --- a/src/NuGetGallery/App_Start/StorageDependent.cs +++ b/src/NuGetGallery/App_Start/StorageDependent.cs @@ -87,6 +87,7 @@ public static IEnumerable GetAll(IAppConfiguration configurati Create(configuration.AzureStorage_UserCertificates_ConnectionString, isSingleInstance: false), Create(configuration.AzureStorage_Content_ConnectionString, isSingleInstance: true), Create(configuration.AzureStorage_Packages_ConnectionString, isSingleInstance: false), + Create(configuration.AzureStorage_Packages_ConnectionString, isSingleInstance: false), Create(configuration.AzureStorage_Uploads_ConnectionString, isSingleInstance: false), }; diff --git a/src/NuGetGallery/Content/gallery/img/git-32x32.png b/src/NuGetGallery/Content/gallery/img/git-32x32.png new file mode 100644 index 0000000000..cb0c77350f Binary files /dev/null and b/src/NuGetGallery/Content/gallery/img/git-32x32.png differ diff --git a/src/NuGetGallery/Content/gallery/img/git.svg b/src/NuGetGallery/Content/gallery/img/git.svg new file mode 100644 index 0000000000..763a4de16d --- /dev/null +++ b/src/NuGetGallery/Content/gallery/img/git.svg @@ -0,0 +1,24 @@ + +image/svg+xml \ No newline at end of file diff --git a/src/NuGetGallery/Content/gallery/img/github-32x32.png b/src/NuGetGallery/Content/gallery/img/github-32x32.png new file mode 100644 index 0000000000..8b25551a97 Binary files /dev/null and b/src/NuGetGallery/Content/gallery/img/github-32x32.png differ diff --git a/src/NuGetGallery/Content/gallery/img/github.svg b/src/NuGetGallery/Content/gallery/img/github.svg new file mode 100644 index 0000000000..c24b330698 --- /dev/null +++ b/src/NuGetGallery/Content/gallery/img/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/NuGetGallery/Controllers/AccountsController.cs b/src/NuGetGallery/Controllers/AccountsController.cs index 6fed67835b..27be140c32 100644 --- a/src/NuGetGallery/Controllers/AccountsController.cs +++ b/src/NuGetGallery/Controllers/AccountsController.cs @@ -92,7 +92,7 @@ public virtual ActionResult ConfirmationRequired(string accountName = null) [HttpPost] [ActionName("ConfirmationRequired")] [ValidateAntiForgeryToken] - public virtual ActionResult ConfirmationRequiredPost(string accountName = null) + public virtual async Task ConfirmationRequiredPost(string accountName = null) { var account = GetAccount(accountName); @@ -108,7 +108,7 @@ public virtual ActionResult ConfirmationRequiredPost(string accountName = null) ConfirmationViewModel model; if (!alreadyConfirmed) { - SendNewAccountEmail(account); + await SendNewAccountEmailAsync(account); model = new ConfirmationViewModel(account) { @@ -122,7 +122,7 @@ public virtual ActionResult ConfirmationRequiredPost(string accountName = null) return View(model); } - protected abstract void SendNewAccountEmail(User account); + protected abstract Task SendNewAccountEmailAsync(User account); [UIAuthorize(allowDiscontinuedLogins: true)] public virtual async Task Confirm(string accountName, string token) @@ -163,7 +163,7 @@ public virtual async Task Confirm(string accountName, string token // Change notice not required for new accounts. if (model.SuccessfulConfirmation && !model.ConfirmingNewAccount) { - MessageService.SendEmailChangeNoticeToPreviousEmailAddress(account, existingEmail); + await MessageService.SendEmailChangeNoticeToPreviousEmailAddressAsync(account, existingEmail); string returnUrl = HttpContext.GetConfirmationReturnUrl(); if (!String.IsNullOrEmpty(returnUrl)) @@ -254,13 +254,13 @@ public virtual async Task ChangeEmail(TAccountViewModel model) if (account.Confirmed) { - SendEmailChangedConfirmationNotice(account); + await SendEmailChangedConfirmationNoticeAsync(account); } return RedirectToAction(AccountAction); } - protected abstract void SendEmailChangedConfirmationNotice(User account); + protected abstract Task SendEmailChangedConfirmationNoticeAsync(User account); [HttpPost] [UIAuthorize] diff --git a/src/NuGetGallery/Controllers/ApiController.cs b/src/NuGetGallery/Controllers/ApiController.cs index b40da0b2a0..7971c18f04 100644 --- a/src/NuGetGallery/Controllers/ApiController.cs +++ b/src/NuGetGallery/Controllers/ApiController.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Globalization; @@ -54,6 +55,9 @@ public partial class ApiController public IReservedNamespaceService ReservedNamespaceService { get; set; } public IPackageUploadService PackageUploadService { get; set; } public IPackageDeleteService PackageDeleteService { get; set; } + public ISymbolPackageService SymbolPackageService { get; set; } + public ISymbolPackageUploadService SymbolPackageUploadService { get; set; } + public IContentObjectService ContentObjectService { get; set; } protected ApiController() { @@ -80,7 +84,10 @@ public ApiController( ISecurityPolicyService securityPolicies, IReservedNamespaceService reservedNamespaceService, IPackageUploadService packageUploadService, - IPackageDeleteService packageDeleteService) + IPackageDeleteService packageDeleteService, + ISymbolPackageService symbolPackageService, + ISymbolPackageUploadService symbolPackageUploadService, + IContentObjectService contentObjectService) { ApiScopeEvaluator = apiScopeEvaluator; EntitiesContext = entitiesContext; @@ -102,6 +109,9 @@ public ApiController( ReservedNamespaceService = reservedNamespaceService; PackageUploadService = packageUploadService; StatisticsService = null; + SymbolPackageService = symbolPackageService; + SymbolPackageUploadService = symbolPackageUploadService; + ContentObjectService = contentObjectService; } public ApiController( @@ -125,11 +135,15 @@ public ApiController( ISecurityPolicyService securityPolicies, IReservedNamespaceService reservedNamespaceService, IPackageUploadService packageUploadService, - IPackageDeleteService packageDeleteService) + IPackageDeleteService packageDeleteService, + ISymbolPackageService symbolPackageService, + ISymbolPackageUploadService symbolPackageUploadServivce, + IContentObjectService contentObjectService) : this(apiScopeEvaluator, entitiesContext, packageService, packageFileService, userService, contentService, indexingService, searchService, autoCuratePackage, statusService, messageService, auditingService, configurationService, telemetryService, authenticationService, credentialBuilder, securityPolicies, - reservedNamespaceService, packageUploadService, packageDeleteService) + reservedNamespaceService, packageUploadService, packageDeleteService, symbolPackageService, symbolPackageUploadServivce, + contentObjectService) { StatisticsService = statisticsService; } @@ -333,6 +347,158 @@ public virtual Task CreatePackagePost() return CreatePackageInternal(); } + [HttpPut] + [ApiAuthorize] + [ApiScopeRequired(NuGetScopes.PackagePush, NuGetScopes.PackagePushVersion)] + [ActionName("PushSymbolPackageApi")] + public virtual async Task CreateSymbolPackagePutAsync() + { + try + { + // Get the user + var currentUser = GetCurrentUser(); + + // Check if symbol package upload is allowed for this user. + if (!ContentObjectService.SymbolsConfiguration.IsSymbolsUploadEnabledForUser(currentUser)) + { + return new HttpStatusCodeWithBodyResult(HttpStatusCode.Unauthorized, Strings.SymbolsPackage_UploadNotAllowed); + } + + // Read symbol package + using (var symbolPackageStream = ReadPackageFromRequest()) + { + try + { + if (FoundEntryInFuture(symbolPackageStream, out ZipArchiveEntry entryInTheFuture)) + { + return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, string.Format( + CultureInfo.CurrentCulture, + Strings.PackageEntryFromTheFuture, + entryInTheFuture.Name)); + } + + using (var packageToPush = new PackageArchiveReader(symbolPackageStream, leaveStreamOpen: false)) + { + var nuspec = packageToPush.GetNuspecReader(); + var id = nuspec.GetId(); + var version = nuspec.GetVersion(); + + // Ensure the corresponding package exists before pushing a snupkg. + var package = PackageService.FindPackageByIdAndVersionStrict(id, version.ToStringSafe()); + if (package == null) + { + return new HttpStatusCodeWithBodyResult(HttpStatusCode.NotFound, string.Format( + CultureInfo.CurrentCulture, + Strings.SymbolsPackage_PackageIdAndVersionNotFound, + id, + version.ToNormalizedStringSafe())); + } + + // Check if this user has the permissions to push the corresponding symbol package + var apiScopeEvaluationResult = EvaluateApiScope(ActionsRequiringPermissions.UploadSymbolPackage, package.PackageRegistration, NuGetScopes.PackagePushVersion, NuGetScopes.PackagePush); + if (!apiScopeEvaluationResult.IsSuccessful()) + { + // User cannot push a symbol package as the current user's scopes does not allow it to push for the corresponding package. + return GetHttpResultFromFailedApiScopeEvaluationForPush(apiScopeEvaluationResult, id, version); + } + + // Do not allow to upload snupkg to a package which has symbols package pending validations. + if (package.SymbolPackages.Any(sp => sp.StatusKey == PackageStatus.Validating)) + { + return new HttpStatusCodeWithBodyResult(HttpStatusCode.Conflict, Strings.SymbolsPackage_ConflictValidating); + } + + try + { + await SymbolPackageService.EnsureValidAsync(packageToPush); + } + catch (Exception ex) + { + ex.Log(); + + var message = Strings.SymbolsPackage_FailedToReadPackage; + if (ex is InvalidPackageException || ex is InvalidDataException || ex is EntityException) + { + message = ex.Message; + } + + return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, message); + } + + var packageStreamMetadata = new PackageStreamMetadata + { + HashAlgorithm = CoreConstants.Sha512HashAlgorithmId, + Hash = CryptographyService.GenerateHash( + symbolPackageStream.AsSeekableStream(), + CoreConstants.Sha512HashAlgorithmId), + Size = symbolPackageStream.Length + }; + + PackageCommitResult commitResult = await SymbolPackageUploadService.CreateAndUploadSymbolsPackage( + package, + packageStreamMetadata, + symbolPackageStream.AsSeekableStream()); + + switch (commitResult) + { + case PackageCommitResult.Success: + break; + case PackageCommitResult.Conflict: + return new HttpStatusCodeWithBodyResult( + HttpStatusCode.Conflict, + Strings.SymbolsPackage_ConflictValidating); + default: + throw new NotImplementedException($"The symbol package commit result {commitResult} is not supported."); + } + + return new HttpStatusCodeResult(HttpStatusCode.Created); + } + } + catch (Exception ex) when (ex is InvalidPackageException + || ex is InvalidDataException + || ex is EntityException + || ex is FrameworkException) + { + return BadRequestForExceptionMessage(ex); + } + } + } + catch (HttpException ex) when (ex.IsMaxRequestLengthExceeded()) + { + // ASP.NET throws HttpException when maxRequestLength limit is exceeded. + return new HttpStatusCodeWithBodyResult( + HttpStatusCode.RequestEntityTooLarge, + Strings.PackageFileTooLarge); + } + catch (Exception ex) + { + ex.Log(); + + throw ex; + } + } + + private bool FoundEntryInFuture(Stream stream, out ZipArchiveEntry entry) + { + entry = null; + + using (var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true)) + { + var reference = DateTime.UtcNow.AddDays(1); // allow "some" clock skew + + var entryInTheFuture = archive.Entries.FirstOrDefault( + e => e.LastWriteTime.UtcDateTime > reference); + + if (entryInTheFuture != null) + { + entry = entryInTheFuture; + return true; + } + } + + return false; + } + private async Task CreatePackageInternal() { string id = null; @@ -353,20 +519,12 @@ private async Task CreatePackageInternal() { try { - using (var archive = new ZipArchive(packageStream, ZipArchiveMode.Read, leaveOpen: true)) + if (FoundEntryInFuture(packageStream, out ZipArchiveEntry entryInTheFuture)) { - var reference = DateTime.UtcNow.AddDays(1); // allow "some" clock skew - - var entryInTheFuture = archive.Entries.FirstOrDefault( - e => e.LastWriteTime.UtcDateTime > reference); - - if (entryInTheFuture != null) - { - return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, string.Format( - CultureInfo.CurrentCulture, - Strings.PackageEntryFromTheFuture, - entryInTheFuture.Name)); - } + return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, string.Format( + CultureInfo.CurrentCulture, + Strings.PackageEntryFromTheFuture, + entryInTheFuture.Name)); } using (var packageToPush = new PackageArchiveReader(packageStream, leaveStreamOpen: false)) @@ -457,10 +615,10 @@ await AuditingService.SaveAuditRecordAsync( TelemetryService.TrackPackageReupload(existingPackage); await PackageDeleteService.HardDeletePackagesAsync( - new[] { existingPackage }, + new[] { existingPackage }, currentUser, - Strings.FailedValidationHardDeleteReason, - Strings.AutomatedPackageDeleteSignature, + Strings.FailedValidationHardDeleteReason, + Strings.AutomatedPackageDeleteSignature, deleteEmptyPackageRegistration: false); } else @@ -473,6 +631,14 @@ await PackageDeleteService.HardDeletePackagesAsync( } } + // Perform all the validations we can before adding the package to the entity context. + var beforeValidationResult = await PackageUploadService.ValidateBeforeGeneratePackageAsync(packageToPush); + var beforeValidationActionResult = GetActionResultOrNull(beforeValidationResult); + if (beforeValidationActionResult != null) + { + return beforeValidationActionResult; + } + var packageStreamMetadata = new PackageStreamMetadata { HashAlgorithm = CoreConstants.Sha512HashAlgorithmId, @@ -489,21 +655,16 @@ await PackageDeleteService.HardDeletePackagesAsync( owner, currentUser); - var validationResult = await PackageUploadService.ValidatePackageAsync( + // Perform validations that require the package already being in the entity context. + var afterValidationResult = await PackageUploadService.ValidateAfterGeneratePackageAsync( package, packageToPush, owner, currentUser); - switch (validationResult.Type) + var afterValidationActionResult = GetActionResultOrNull(afterValidationResult); + if (afterValidationActionResult != null) { - case PackageValidationResultType.Accepted: - break; - case PackageValidationResultType.Invalid: - case PackageValidationResultType.PackageShouldNotBeSigned: - case PackageValidationResultType.PackageShouldNotBeSignedButCanManageCertificates: - return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, validationResult.Message); - default: - throw new NotImplementedException($"The package validation result type {validationResult.Type} is not supported."); + return afterValidationActionResult; } await AutoCuratePackage.ExecuteAsync(package, packageToPush, commitChanges: false); @@ -538,7 +699,7 @@ await AuditingService.SaveAuditRecordAsync( if (!(ConfigurationService.Current.AsynchronousPackageValidationEnabled && ConfigurationService.Current.BlockingAsynchronousPackageValidationEnabled)) { // Notify user of push unless async validation in blocking mode is used - MessageService.SendPackageAddedNotice(package, + await MessageService.SendPackageAddedNoticeAsync(package, Url.Package(package.PackageRegistration.Id, package.NormalizedVersion, relativeUrl: false), Url.ReportPackage(package.PackageRegistration.Id, package.NormalizedVersion, relativeUrl: false), Url.AccountSettings(relativeUrl: false)); @@ -546,12 +707,15 @@ await AuditingService.SaveAuditRecordAsync( TelemetryService.TrackPackagePushEvent(package, currentUser, User.Identity); + var warnings = new List(); + warnings.AddRange(beforeValidationResult.Warnings); + warnings.AddRange(afterValidationResult.Warnings); if (package.SemVerLevelKey == SemVerLevelKey.SemVer2) { - return new HttpStatusCodeWithServerWarningResult(HttpStatusCode.Created, Strings.WarningSemVer2PackagePushed); + warnings.Add(Strings.WarningSemVer2PackagePushed); } - return new HttpStatusCodeResult(HttpStatusCode.Created); + return new HttpStatusCodeWithServerWarningResult(HttpStatusCode.Created, warnings); } } catch (InvalidPackageException ex) @@ -586,6 +750,21 @@ await AuditingService.SaveAuditRecordAsync( } } + private static ActionResult GetActionResultOrNull(PackageValidationResult validationResult) + { + switch (validationResult.Type) + { + case PackageValidationResultType.Accepted: + return null; + case PackageValidationResultType.Invalid: + case PackageValidationResultType.PackageShouldNotBeSigned: + case PackageValidationResultType.PackageShouldNotBeSignedButCanManageCertificates: + return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, validationResult.Message); + default: + throw new NotImplementedException($"The package validation result type {validationResult.Type} is not supported."); + } + } + private static ActionResult BadRequestForExceptionMessage(Exception ex) { return new HttpStatusCodeWithBodyResult( @@ -819,8 +998,8 @@ private HttpStatusCodeWithBodyResult GetHttpResultFromFailedApiScopeEvaluationHe TelemetryService.TrackPackagePushNamespaceConflictEvent(id, version.ToNormalizedString(), GetCurrentUser(), User.Identity); return new HttpStatusCodeWithBodyResult(HttpStatusCode.Conflict, Strings.UploadPackage_IdNamespaceConflict); } - - var message = result.PermissionsCheckResult == PermissionsCheckResult.Allowed && !result.IsOwnerConfirmed ? + + var message = result.PermissionsCheckResult == PermissionsCheckResult.Allowed && !result.IsOwnerConfirmed ? Strings.ApiKeyOwnerUnconfirmed : Strings.ApiKeyNotAuthorized; return new HttpStatusCodeWithBodyResult(statusCodeOnFailure, message); diff --git a/src/NuGetGallery/Controllers/AuthenticationController.cs b/src/NuGetGallery/Controllers/AuthenticationController.cs index 0218a76257..4cf2b03841 100644 --- a/src/NuGetGallery/Controllers/AuthenticationController.cs +++ b/src/NuGetGallery/Controllers/AuthenticationController.cs @@ -287,7 +287,7 @@ public virtual async Task Register(LogOnViewModel model, string re // Send a new account email if (NuGetContext.Config.Current.ConfirmEmailAddresses && !string.IsNullOrEmpty(user.User.UnconfirmedEmailAddress)) { - _messageService.SendNewAccountEmail( + await _messageService.SendNewAccountEmailAsync( user.User, Url.ConfirmEmail( user.User.Username, @@ -325,7 +325,7 @@ public virtual ActionResult LogOff(string returnUrl) [HttpPost] [ValidateAntiForgeryToken] - public virtual JsonResult SignInAssistance(string username, string providedEmailAddress) + public virtual async Task SignInAssistance(string username, string providedEmailAddress) { // If provided email address is empty or null, return the result with a formatted // email address, otherwise send sign-in assistance email to the associated mail address. @@ -352,7 +352,7 @@ public virtual JsonResult SignInAssistance(string username, string providedEmail else { var externalCredentials = user.Credentials.Where(cred => cred.IsExternal()); - _messageService.SendSigninAssistanceEmail(new MailAddress(email, user.Username), externalCredentials); + await _messageService.SendSigninAssistanceEmailAsync(new MailAddress(email, user.Username), externalCredentials); return Json(new { success = true }); } } @@ -674,7 +674,7 @@ private async Task AssociateCredential(AuthenticatedUser user) await RemovePasswordCredential(user.User); // Notify the user of the change - _messageService.SendCredentialAddedNotice(user.User, _authService.DescribeCredential(result.Credential)); + await _messageService.SendCredentialAddedNoticeAsync(user.User, _authService.DescribeCredential(result.Credential)); return new LoginUserDetails { diff --git a/src/NuGetGallery/Controllers/JsonApiController.cs b/src/NuGetGallery/Controllers/JsonApiController.cs index c9fb270c53..248444737e 100644 --- a/src/NuGetGallery/Controllers/JsonApiController.cs +++ b/src/NuGetGallery/Controllers/JsonApiController.cs @@ -119,7 +119,7 @@ public async Task AddPackageOwner(string id, string username, string foreach (var owner in model.Package.Owners) { - _messageService.SendPackageOwnerAddedNotice(owner, model.User, model.Package, packageUrl); + await _messageService.SendPackageOwnerAddedNoticeAsync(owner, model.User, model.Package, packageUrl); } } else @@ -147,12 +147,12 @@ public async Task AddPackageOwner(string id, string username, string model.User.Username, relativeUrl: false); - _messageService.SendPackageOwnerRequest(model.CurrentUser, model.User, model.Package, packageUrl, + await _messageService.SendPackageOwnerRequestAsync(model.CurrentUser, model.User, model.Package, packageUrl, confirmationUrl, rejectionUrl, encodedMessage, policyMessage: string.Empty); foreach (var owner in model.Package.Owners) { - _messageService.SendPackageOwnerRequestInitiatedNotice(model.CurrentUser, owner, model.User, model.Package, cancellationUrl); + await _messageService.SendPackageOwnerRequestInitiatedNoticeAsync(model.CurrentUser, owner, model.User, model.Package, cancellationUrl); } } @@ -190,12 +190,12 @@ public async Task RemovePackageOwner(string id, string username) throw new InvalidOperationException("You can't remove the only owner from a package."); } await _packageOwnershipManagementService.RemovePackageOwnerAsync(model.Package, model.CurrentUser, model.User, commitAsTransaction:true); - _messageService.SendPackageOwnerRemovedNotice(model.CurrentUser, model.User, model.Package); + await _messageService.SendPackageOwnerRemovedNoticeAsync(model.CurrentUser, model.User, model.Package); } else { await _packageOwnershipManagementService.DeletePackageOwnershipRequestAsync(model.Package, model.User); - _messageService.SendPackageOwnerRequestCancellationNotice(model.CurrentUser, model.User, model.Package); + await _messageService.SendPackageOwnerRequestCancellationNoticeAsync(model.CurrentUser, model.User, model.Package); } return Json(new { success = true }); diff --git a/src/NuGetGallery/Controllers/OrganizationsController.cs b/src/NuGetGallery/Controllers/OrganizationsController.cs index d007a4ea2b..c5ef85db72 100644 --- a/src/NuGetGallery/Controllers/OrganizationsController.cs +++ b/src/NuGetGallery/Controllers/OrganizationsController.cs @@ -53,17 +53,17 @@ public OrganizationsController( EmailUpdateCancelled = Strings.OrganizationEmailUpdateCancelled }; - protected override void SendNewAccountEmail(User account) + protected override Task SendNewAccountEmailAsync(User account) { var confirmationUrl = Url.ConfirmOrganizationEmail(account.Username, account.EmailConfirmationToken, relativeUrl: false); - MessageService.SendNewAccountEmail(account, confirmationUrl); + return MessageService.SendNewAccountEmailAsync(account, confirmationUrl); } - protected override void SendEmailChangedConfirmationNotice(User account) + protected override Task SendEmailChangedConfirmationNoticeAsync(User account) { var confirmationUrl = Url.ConfirmOrganizationEmail(account.Username, account.EmailConfirmationToken, relativeUrl: false); - MessageService.SendEmailChangeConfirmationNotice(account, confirmationUrl); + return MessageService.SendEmailChangeConfirmationNoticeAsync(account, confirmationUrl); } [HttpGet] @@ -85,7 +85,7 @@ public async Task Add(AddOrganizationViewModel model) try { var organization = await UserService.AddOrganizationAsync(organizationName, organizationEmailAddress, adminUser); - SendNewAccountEmail(organization); + await SendNewAccountEmailAsync(organization); TelemetryService.TrackOrganizationAdded(organization); return RedirectToAction(nameof(ManageOrganization), new { accountName = organization.Username }); } @@ -134,8 +134,8 @@ public async Task AddMember(string accountName, string memberName, b var rejectUrl = Url.RejectOrganizationMembershipRequest(request, relativeUrl: false); var cancelUrl = Url.CancelOrganizationMembershipRequest(memberName, relativeUrl: false); - MessageService.SendOrganizationMembershipRequest(account, request.NewMember, currentUser, request.IsAdmin, profileUrl, confirmUrl, rejectUrl); - MessageService.SendOrganizationMembershipRequestInitiatedNotice(account, currentUser, request.NewMember, request.IsAdmin, cancelUrl); + await MessageService.SendOrganizationMembershipRequestAsync(account, request.NewMember, currentUser, request.IsAdmin, profileUrl, confirmUrl, rejectUrl); + await MessageService.SendOrganizationMembershipRequestInitiatedNoticeAsync(account, currentUser, request.NewMember, request.IsAdmin, cancelUrl); return Json(new OrganizationMemberViewModel(request)); } @@ -159,7 +159,7 @@ public async Task ConfirmMemberRequest(string accountName, string try { var member = await UserService.AddMemberAsync(account, GetCurrentUser().Username, confirmationToken); - MessageService.SendOrganizationMemberUpdatedNotice(account, member); + await MessageService.SendOrganizationMemberUpdatedNoticeAsync(account, member); TempData["Message"] = String.Format(CultureInfo.CurrentCulture, Strings.AddMember_Success, account.Username); @@ -188,7 +188,7 @@ public async Task RejectMemberRequest(string accountName, string c { var member = GetCurrentUser(); await UserService.RejectMembershipRequestAsync(account, member.Username, confirmationToken); - MessageService.SendOrganizationMembershipRequestRejectedNotice(account, member); + await MessageService.SendOrganizationMembershipRequestRejectedNoticeAsync(account, member); return HandleOrganizationMembershipRequestView(new HandleOrganizationMembershipRequestModel(false, account)); } @@ -221,7 +221,7 @@ public async Task CancelMemberRequest(string accountName, string mem try { var removedUser = await UserService.CancelMembershipRequestAsync(account, memberName); - MessageService.SendOrganizationMembershipRequestCancelledNotice(account, removedUser); + await MessageService.SendOrganizationMembershipRequestCancelledNoticeAsync(account, removedUser); return Json(Strings.CancelMemberRequest_Success); } catch (EntityException e) @@ -252,7 +252,7 @@ public async Task UpdateMember(string accountName, string memberName try { var membership = await UserService.UpdateMemberAsync(account, memberName, isAdmin); - MessageService.SendOrganizationMemberUpdatedNotice(account, membership); + await MessageService.SendOrganizationMemberUpdatedNoticeAsync(account, membership); return Json(new OrganizationMemberViewModel(membership)); } @@ -287,7 +287,7 @@ public async Task DeleteMember(string accountName, string memberName try { var removedMember = await UserService.DeleteMemberAsync(account, memberName); - MessageService.SendOrganizationMemberRemovedNotice(account, removedMember); + await MessageService.SendOrganizationMemberRemovedNoticeAsync(account, removedMember); return Json(Strings.DeleteMember_Success); } catch (EntityException e) diff --git a/src/NuGetGallery/Controllers/PackagesController.cs b/src/NuGetGallery/Controllers/PackagesController.cs index a22b9859f7..6b7bc386d7 100644 --- a/src/NuGetGallery/Controllers/PackagesController.cs +++ b/src/NuGetGallery/Controllers/PackagesController.cs @@ -160,13 +160,20 @@ public virtual async Task UploadPackage() { if (uploadedFile != null) { - var package = await SafeCreatePackage(currentUser, uploadedFile); if (package == null) { return View(model); } + var validationResult = await _packageUploadService.ValidateBeforeGeneratePackageAsync(package); + var validationErrorMessage = GetErrorMessageOrNull(validationResult); + if (validationErrorMessage != null) + { + TempData["Message"] = validationErrorMessage; + return View(model); + } + try { packageMetadata = PackageMetadata.FromNuspecReader( @@ -181,8 +188,6 @@ public virtual async Task UploadPackage() return View(model); } - model.IsUploadInProgress = true; - var existingPackageRegistration = _packageService.FindPackageRegistrationById(packageMetadata.Id); bool isAllowed; IEnumerable accountsAllowedOnBehalfOf = Enumerable.Empty(); @@ -204,6 +209,7 @@ public virtual async Task UploadPackage() } var verifyRequest = new VerifyPackageRequest(packageMetadata, accountsAllowedOnBehalfOf, existingPackageRegistration); + verifyRequest.Warnings.AddRange(validationResult.Warnings); model.InProgressUpload = verifyRequest; } @@ -230,13 +236,11 @@ public virtual async Task UploadPackage(HttpPostedFileBase uploadFil if (uploadFile == null) { - ModelState.AddModelError(String.Empty, Strings.UploadFileIsRequired); return Json(HttpStatusCode.BadRequest, new[] { Strings.UploadFileIsRequired }); } if (!Path.GetExtension(uploadFile.FileName).Equals(CoreConstants.NuGetPackageFileExtension, StringComparison.OrdinalIgnoreCase)) { - ModelState.AddModelError(String.Empty, Strings.UploadFileMustBeNuGetPackage); return Json(HttpStatusCode.BadRequest, new[] { Strings.UploadFileMustBeNuGetPackage }); } @@ -257,11 +261,6 @@ public virtual async Task UploadPackage(HttpPostedFileBase uploadFil if (entryInTheFuture != null) { - ModelState.AddModelError(String.Empty, string.Format( - CultureInfo.CurrentCulture, - Strings.PackageEntryFromTheFuture, - entryInTheFuture.Name)); - return Json(HttpStatusCode.BadRequest, new[] { string.Format(CultureInfo.CurrentCulture, Strings.PackageEntryFromTheFuture, entryInTheFuture.Name) }); } @@ -284,8 +283,6 @@ public virtual async Task UploadPackage(HttpPostedFileBase uploadFil message = ex.Message; } - ModelState.AddModelError(String.Empty, message); - return Json(HttpStatusCode.BadRequest, new[] { message }); } finally @@ -301,22 +298,14 @@ public virtual async Task UploadPackage(HttpPostedFileBase uploadFil foreach (var error in errors) { errorStrings.Add(error.ErrorMessage); - ModelState.AddModelError(String.Empty, error.ErrorMessage); } - return Json(HttpStatusCode.BadRequest, errorStrings); + return Json(HttpStatusCode.BadRequest, errorStrings.ToArray()); } // Check min client version if (nuspec.GetMinClientVersion() > Constants.MaxSupportedMinClientVersion) { - ModelState.AddModelError( - string.Empty, - string.Format( - CultureInfo.CurrentCulture, - Strings.UploadPackage_MinClientVersionOutOfRange, - nuspec.GetMinClientVersion())); - return Json(HttpStatusCode.BadRequest, new[] { string.Format(CultureInfo.CurrentCulture, Strings.UploadPackage_MinClientVersionOutOfRange, nuspec.GetMinClientVersion()) }); } @@ -328,9 +317,6 @@ public virtual async Task UploadPackage(HttpPostedFileBase uploadFil ActionsRequiringPermissions.UploadNewPackageId.CheckPermissionsOnBehalfOfAnyAccount( currentUser, new ActionOnNewPackageContext(id, _reservedNamespaceService), out accountsAllowedOnBehalfOf) != PermissionsCheckResult.Allowed) { - ModelState.AddModelError( - string.Empty, string.Format(CultureInfo.CurrentCulture, Strings.UploadPackage_IdNamespaceConflict)); - var version = nuspec.GetVersion().ToNormalizedString(); _telemetryService.TrackPackagePushNamespaceConflictEvent(id, version, currentUser, User.Identity); @@ -343,9 +329,6 @@ public virtual async Task UploadPackage(HttpPostedFileBase uploadFil if (ActionsRequiringPermissions.UploadNewPackageVersion.CheckPermissionsOnBehalfOfAnyAccount( currentUser, existingPackageRegistration, out accountsAllowedOnBehalfOf) != PermissionsCheckResult.Allowed) { - ModelState.AddModelError( - string.Empty, string.Format(CultureInfo.CurrentCulture, Strings.PackageIdNotAvailable, existingPackageRegistration.Id)); - return Json(HttpStatusCode.Conflict, new[] { string.Format(CultureInfo.CurrentCulture, Strings.PackageIdNotAvailable, existingPackageRegistration.Id) }); } @@ -395,10 +378,6 @@ await _packageDeleteService.HardDeletePackagesAsync( existingPackage.Version); } - ModelState.AddModelError( - string.Empty, - message); - return Json(HttpStatusCode.Conflict, new[] { message }); } } @@ -407,11 +386,11 @@ await _packageDeleteService.HardDeletePackagesAsync( } PackageMetadata packageMetadata; + IReadOnlyList warnings; using (Stream uploadedFile = await _uploadFileService.GetUploadFileAsync(currentUser.Key)) { if (uploadedFile == null) { - ModelState.AddModelError(String.Empty, Strings.UploadFileIsRequired); return Json(HttpStatusCode.BadRequest, new[] { Strings.UploadFileIsRequired }); } @@ -433,9 +412,19 @@ await _packageDeleteService.HardDeletePackagesAsync( return Json(HttpStatusCode.BadRequest, new[] { ex.GetUserSafeMessage() }); } + + var validationResult = await _packageUploadService.ValidateBeforeGeneratePackageAsync(package); + var validationJsonResult = GetJsonResultOrNull(validationResult); + if (validationJsonResult != null) + { + return validationJsonResult; + } + + warnings = validationResult.Warnings; } var model = new VerifyPackageRequest(packageMetadata, accountsAllowedOnBehalfOf, existingPackageRegistration); + model.Warnings.AddRange(warnings); return Json(model); } @@ -751,7 +740,7 @@ public virtual async Task ReportAbuse(string id, string version, R await _supportRequestService.AddNewSupportRequestAsync(subject, reportForm.Message, requestorEmailAddress, reason, user, package); - _messageService.ReportAbuse(request); + await _messageService.ReportAbuseAsync(request); TempData["Message"] = "Your abuse report has been sent to the gallery operators."; @@ -811,7 +800,7 @@ public virtual async Task ReportMyPackage(string id, string versio if (!deleted) { - NotifyReportMyPackageSupportRequest(reportForm, package, user, from); + await NotifyReportMyPackageSupportRequestAsync(reportForm, package, user, from); } return Redirect(Url.Package(package.PackageRegistration.Id, package.NormalizedVersion)); @@ -879,7 +868,7 @@ private async Task ValidateReportMyPackageViewModel(ReportMyPackag return null; } - private void NotifyReportMyPackageSupportRequest(ReportMyPackageViewModel reportForm, Package package, User user, MailAddress from) + private async Task NotifyReportMyPackageSupportRequestAsync(ReportMyPackageViewModel reportForm, Package package, User user, MailAddress from) { var request = new ReportPackageRequest { @@ -892,7 +881,7 @@ private void NotifyReportMyPackageSupportRequest(ReportMyPackageViewModel report CopySender = reportForm.CopySender }; - _messageService.ReportMyPackage(request); + await _messageService.ReportMyPackageAsync(request); TempData["Message"] = Strings.SupportRequestSentTransientMessage; } @@ -930,7 +919,7 @@ await _supportRequestService.UpdateIssueAsync( comment: null, editedBy: user.Username); - _messageService.SendPackageDeletedNotice( + await _messageService.SendPackageDeletedNoticeAsync( package, Url.Package(package.PackageRegistration.Id, package.NormalizedVersion, relativeUrl: false), Url.ReportPackage(package.PackageRegistration.Id, package.NormalizedVersion, relativeUrl: false)); @@ -971,7 +960,7 @@ public virtual ActionResult ContactOwners(string id, string version) [ValidateAntiForgeryToken] [ValidateRecaptchaResponse] [RequiresAccountConfirmation("contact package owners")] - public virtual ActionResult ContactOwners(string id, string version, ContactOwnersViewModel contactForm) + public virtual async Task ContactOwners(string id, string version, ContactOwnersViewModel contactForm) { // Html Encode the message contactForm.Message = System.Web.HttpUtility.HtmlEncode(contactForm.Message); @@ -989,7 +978,7 @@ public virtual ActionResult ContactOwners(string id, string version, ContactOwne var user = GetCurrentUser(); var fromAddress = new MailAddress(user.EmailAddress, user.Username); - _messageService.SendContactOwnersMessage( + await _messageService.SendContactOwnersMessageAsync( fromAddress, package, Url.Package(package, false), @@ -1355,7 +1344,7 @@ private async Task HandleOwnershipRequest(string id, string userna { await _packageOwnershipManagementService.AddPackageOwnerAsync(package, user); - SendAddPackageOwnerNotification(package, user); + await SendAddPackageOwnerNotificationAsync(package, user); return View("ConfirmOwner", new PackageOwnerConfirmationModel(id, user.Username, ConfirmOwnershipResult.Success)); } @@ -1365,7 +1354,7 @@ private async Task HandleOwnershipRequest(string id, string userna await _packageOwnershipManagementService.DeletePackageOwnershipRequestAsync(package, user); - _messageService.SendPackageOwnerRequestRejectionNotice(requestingUser, user, package); + await _messageService.SendPackageOwnerRequestRejectionNoticeAsync(requestingUser, user, package); return View("ConfirmOwner", new PackageOwnerConfirmationModel(id, user.Username, ConfirmOwnershipResult.Rejected)); } @@ -1407,7 +1396,7 @@ public virtual async Task CancelPendingOwnershipRequest(string id, await _packageOwnershipManagementService.DeletePackageOwnershipRequestAsync(package, pendingUser); - _messageService.SendPackageOwnerRequestCancellationNotice(requestingUser, pendingUser, package); + await _messageService.SendPackageOwnerRequestCancellationNoticeAsync(requestingUser, pendingUser, package); return View("ConfirmOwner", new PackageOwnerConfirmationModel(id, pendingUsername, ConfirmOwnershipResult.Cancelled)); } @@ -1417,14 +1406,15 @@ public virtual async Task CancelPendingOwnershipRequest(string id, /// /// Package to which owner was added. /// Owner added. - private void SendAddPackageOwnerNotification(PackageRegistration package, User newOwner) + private Task SendAddPackageOwnerNotificationAsync(PackageRegistration package, User newOwner) { var packageUrl = Url.Package(package.Id, version: null, relativeUrl: false); Func notNewOwner = o => !o.Username.Equals(newOwner.Username, StringComparison.OrdinalIgnoreCase); // Notify existing owners var notNewOwners = package.Owners.Where(notNewOwner).ToList(); - notNewOwners.ForEach(owner => _messageService.SendPackageOwnerAddedNotice(owner, newOwner, package, packageUrl)); + var tasks = notNewOwners.Select(owner => _messageService.SendPackageOwnerAddedNoticeAsync(owner, newOwner, package, packageUrl)); + return Task.WhenAll(tasks); } internal virtual async Task Edit(string id, string version, bool? listed, Func urlFactory) @@ -1602,6 +1592,14 @@ public virtual async Task VerifyPackage(VerifyPackageRequest formDat } } + // Perform all the validations we can before adding the package to the entity context. + var beforeValidationResult = await _packageUploadService.ValidateBeforeGeneratePackageAsync(nugetPackage); + var beforeValidationJsonResult = GetJsonResultOrNull(beforeValidationResult); + if (beforeValidationJsonResult != null) + { + return beforeValidationJsonResult; + } + // update relevant database tables try { @@ -1621,28 +1619,16 @@ public virtual async Task VerifyPackage(VerifyPackageRequest formDat return Json(HttpStatusCode.BadRequest, new[] { ex.Message }); } - var validationResult = await _packageUploadService.ValidatePackageAsync( + // Perform validations that require the package already being in the entity context. + var afterValidationResult = await _packageUploadService.ValidateAfterGeneratePackageAsync( package, nugetPackage, owner, currentUser); - switch (validationResult.Type) + var afterValidationJsonResult = GetJsonResultOrNull(afterValidationResult); + if (afterValidationJsonResult != null) { - case PackageValidationResultType.Accepted: - break; - case PackageValidationResultType.Invalid: - case PackageValidationResultType.PackageShouldNotBeSigned: - return Json(HttpStatusCode.BadRequest, new[] { validationResult.Message }); - case PackageValidationResultType.PackageShouldNotBeSignedButCanManageCertificates: - return Json( - HttpStatusCode.BadRequest, - new[] - { - validationResult.Message + " " + - Strings.UploadPackage_PackageIsSignedButMissingCertificate_ManageCertificate - }); - default: - throw new NotImplementedException($"The package validation result type {validationResult.Type} is not supported."); + return afterValidationJsonResult; } if (formData.Edit != null) @@ -1708,7 +1694,7 @@ await _auditingService.SaveAuditRecordAsync( if (!(_config.AsynchronousPackageValidationEnabled && _config.BlockingAsynchronousPackageValidationEnabled)) { // notify user unless async validation in blocking mode is used - _messageService.SendPackageAddedNotice(package, + await _messageService.SendPackageAddedNoticeAsync(package, Url.Package(package.PackageRegistration.Id, package.NormalizedVersion, relativeUrl: false), Url.ReportPackage(package.PackageRegistration.Id, package.NormalizedVersion, relativeUrl: false), Url.AccountSettings(relativeUrl: false)); @@ -1753,6 +1739,34 @@ await _auditingService.SaveAuditRecordAsync( } } + private JsonResult GetJsonResultOrNull(PackageValidationResult validationResult) + { + var errorMessage = GetErrorMessageOrNull(validationResult); + if (errorMessage == null) + { + return null; + } + + return Json(HttpStatusCode.BadRequest, new[] { errorMessage }); + } + + private static string GetErrorMessageOrNull(PackageValidationResult validationResult) + { + switch (validationResult.Type) + { + case PackageValidationResultType.Accepted: + return null; + case PackageValidationResultType.Invalid: + case PackageValidationResultType.PackageShouldNotBeSigned: + return validationResult.Message; + case PackageValidationResultType.PackageShouldNotBeSignedButCanManageCertificates: + return validationResult.Message + " " + + Strings.UploadPackage_PackageIsSignedButMissingCertificate_ManageCertificate; + default: + throw new NotImplementedException($"The package validation result type {validationResult.Type} is not supported."); + } + } + private async Task SafeCreatePackage(User currentUser, Stream uploadFile) { Exception caught = null; diff --git a/src/NuGetGallery/Controllers/PagesController.cs b/src/NuGetGallery/Controllers/PagesController.cs index d10103cd4c..76072a8bc4 100644 --- a/src/NuGetGallery/Controllers/PagesController.cs +++ b/src/NuGetGallery/Controllers/PagesController.cs @@ -94,7 +94,7 @@ public virtual async Task Contact(ContactSupportViewModel contactF var subject = $"Support Request for user '{user.Username}'"; await _supportRequestService.AddNewSupportRequestAsync(subject, contactForm.Message, user.EmailAddress, "Other", user); - _messageService.SendContactSupportEmail(request); + await _messageService.SendContactSupportEmailAsync(request); ModelState.Clear(); diff --git a/src/NuGetGallery/Controllers/UsersController.cs b/src/NuGetGallery/Controllers/UsersController.cs index 1006534acf..ab357566d8 100644 --- a/src/NuGetGallery/Controllers/UsersController.cs +++ b/src/NuGetGallery/Controllers/UsersController.cs @@ -70,17 +70,17 @@ public UsersController( EmailUpdateCancelled = Strings.UserEmailUpdateCancelled }; - protected override void SendNewAccountEmail(User account) + protected override Task SendNewAccountEmailAsync(User account) { var confirmationUrl = Url.ConfirmEmail(account.Username, account.EmailConfirmationToken, relativeUrl: false); - MessageService.SendNewAccountEmail(account, confirmationUrl); + return MessageService.SendNewAccountEmailAsync(account, confirmationUrl); } - protected override void SendEmailChangedConfirmationNotice(User account) + protected override Task SendEmailChangedConfirmationNoticeAsync(User account) { var confirmationUrl = Url.ConfirmEmail(account.Username, account.EmailConfirmationToken, relativeUrl: false); - MessageService.SendEmailChangeConfirmationNotice(account, confirmationUrl); + return MessageService.SendEmailChangeConfirmationNoticeAsync(account, confirmationUrl); } protected override User GetAccount(string accountName) @@ -158,16 +158,16 @@ public virtual async Task TransformToOrganization(TransformAccount if (existingTransformRequestUser != null) { - MessageService.SendOrganizationTransformRequestCancelledNotice(accountToTransform, existingTransformRequestUser); + await MessageService.SendOrganizationTransformRequestCancelledNoticeAsync(accountToTransform, existingTransformRequestUser); } var returnUrl = Url.ConfirmTransformAccount(accountToTransform); var confirmUrl = Url.ConfirmTransformAccount(accountToTransform, relativeUrl: false); var rejectUrl = Url.RejectTransformAccount(accountToTransform, relativeUrl: false); - MessageService.SendOrganizationTransformRequest(accountToTransform, adminUser, Url.User(accountToTransform, relativeUrl: false), confirmUrl, rejectUrl); + await MessageService.SendOrganizationTransformRequestAsync(accountToTransform, adminUser, Url.User(accountToTransform, relativeUrl: false), confirmUrl, rejectUrl); var cancelUrl = Url.CancelTransformAccount(accountToTransform, relativeUrl: false); - MessageService.SendOrganizationTransformInitiatedNotice(accountToTransform, adminUser, cancelUrl); + await MessageService.SendOrganizationTransformInitiatedNoticeAsync(accountToTransform, adminUser, cancelUrl); TelemetryService.TrackOrganizationTransformInitiated(accountToTransform); @@ -206,7 +206,7 @@ public virtual async Task ConfirmTransformToOrganization(string ac return TransformToOrganizationFailed(errorReason); } - MessageService.SendOrganizationTransformRequestAcceptedNotice(accountToTransform, adminUser); + await MessageService.SendOrganizationTransformRequestAcceptedNoticeAsync(accountToTransform, adminUser); TelemetryService.TrackOrganizationTransformCompleted(accountToTransform); @@ -234,7 +234,7 @@ public virtual async Task RejectTransformToOrganization(string acc { if (await UserService.RejectTransformUserToOrganizationRequest(accountToTransform, adminUser, token)) { - MessageService.SendOrganizationTransformRequestRejectedNotice(accountToTransform, adminUser); + await MessageService.SendOrganizationTransformRequestRejectedNoticeAsync(accountToTransform, adminUser); TelemetryService.TrackOrganizationTransformDeclined(accountToTransform); @@ -262,7 +262,7 @@ public virtual async Task CancelTransformToOrganization(string tok if (await UserService.CancelTransformUserToOrganizationRequest(accountToTransform, token)) { - MessageService.SendOrganizationTransformRequestCancelledNotice(accountToTransform, adminUser); + await MessageService.SendOrganizationTransformRequestCancelledNoticeAsync(accountToTransform, adminUser); TelemetryService.TrackOrganizationTransformCancelled(accountToTransform); @@ -314,7 +314,7 @@ public override async Task RequestAccountDeletion(string accountNa var isSupportRequestCreated = await _supportRequestService.TryAddDeleteSupportRequestAsync(user); if (isSupportRequestCreated) { - MessageService.SendAccountDeleteNotice(user); + await MessageService.SendAccountDeleteNoticeAsync(user); } else { @@ -527,7 +527,7 @@ public virtual async Task ForgotPassword(ForgotPasswordViewModel m ModelState.AddModelError(string.Empty, Strings.CouldNotFindAnyoneWithThatUsernameOrEmail); break; case PasswordResetResultType.Success: - return SendPasswordResetEmail(result.User, forgotPassword: true); + return await SendPasswordResetEmailAsync(result.User, forgotPassword: true); default: throw new NotImplementedException($"The password reset result type '{result.Type}' is not supported."); } @@ -597,7 +597,7 @@ public virtual async Task ResetPassword(string username, string to if (credential != null && !forgot) { // Setting a password, so notify the user - MessageService.SendCredentialAddedNotice(credential.User, AuthenticationService.DescribeCredential(credential)); + await MessageService.SendCredentialAddedNoticeAsync(credential.User, AuthenticationService.DescribeCredential(credential)); } return RedirectToAction("PasswordChanged"); @@ -646,7 +646,7 @@ public virtual async Task ChangePassword(UserAccountViewModel mode return AccountView(user, model); } - return SendPasswordResetEmail(user, forgotPassword: false); + return await SendPasswordResetEmailAsync(user, forgotPassword: false); } else { @@ -831,7 +831,7 @@ public virtual async Task GenerateApiKey(string description, string var newCredentialViewModel = await GenerateApiKeyInternal(description, resolvedScopes, expiration); - MessageService.SendCredentialAddedNotice(GetCurrentUser(), newCredentialViewModel); + await MessageService.SendCredentialAddedNoticeAsync(GetCurrentUser(), newCredentialViewModel); return Json(new ApiKeyViewModel(newCredentialViewModel)); } @@ -987,7 +987,7 @@ private async Task RemoveApiKeyCredential(User user, Credential cred await AuthenticationService.RemoveCredential(user, cred); // Notify the user of the change - MessageService.SendCredentialRemovedNotice(user, AuthenticationService.DescribeCredential(cred)); + await MessageService.SendCredentialRemovedNoticeAsync(user, AuthenticationService.DescribeCredential(cred)); return Json(Strings.CredentialRemoved); } @@ -1017,7 +1017,7 @@ private async Task RemoveCredentialInternal(User user, Credential } // Notify the user of the change - MessageService.SendCredentialRemovedNotice(user, AuthenticationService.DescribeCredential(cred)); + await MessageService.SendCredentialRemovedNoticeAsync(user, AuthenticationService.DescribeCredential(cred)); TempData["Message"] = message; } @@ -1058,14 +1058,14 @@ private static int CountLoginCredentials(User user) c.Type.StartsWith(CredentialTypes.External.Prefix, StringComparison.OrdinalIgnoreCase)); } - private ActionResult SendPasswordResetEmail(User user, bool forgotPassword) + private async Task SendPasswordResetEmailAsync(User user, bool forgotPassword) { var resetPasswordUrl = Url.ResetEmailOrPassword( user.Username, user.PasswordResetToken, forgotPassword, relativeUrl: false); - MessageService.SendPasswordResetInstructions(user, resetPasswordUrl, forgotPassword); + await MessageService.SendPasswordResetInstructionsAsync(user, resetPasswordUrl, forgotPassword); return RedirectToAction(actionName: "PasswordSent", controllerName: "Users"); } diff --git a/src/NuGetGallery/Helpers/PackageHelper.cs b/src/NuGetGallery/Helpers/PackageHelper.cs index 2c1b645707..2970c587e6 100644 --- a/src/NuGetGallery/Helpers/PackageHelper.cs +++ b/src/NuGetGallery/Helpers/PackageHelper.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; +using NuGetGallery.Packaging; namespace NuGetGallery { @@ -16,16 +18,113 @@ public static string ParseTags(string tags) return tags.Replace(',', ' ').Replace(';', ' ').Replace('\t', ' ').Replace(" ", " "); } - public static bool ShouldRenderUrl(string url) + public static bool ShouldRenderUrl(string url, bool secureOnly = false) { - Uri uri = null; - if (!string.IsNullOrEmpty(url) && Uri.TryCreate(url, UriKind.Absolute, out uri)) + if (!string.IsNullOrEmpty(url) && Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) { + if (secureOnly) + { + return uri.Scheme == Uri.UriSchemeHttps; + } + return uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp; } return false; } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + public static void ValidateNuGetPackageMetadata(PackageMetadata packageMetadata) + { + // TODO: Change this to use DataAnnotations + if (packageMetadata.Id.Length > CoreConstants.MaxPackageIdLength) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Id", CoreConstants.MaxPackageIdLength); + } + if (packageMetadata.Authors != null && packageMetadata.Authors.Flatten().Length > 4000) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Authors", "4000"); + } + if (packageMetadata.Copyright != null && packageMetadata.Copyright.Length > 4000) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Copyright", "4000"); + } + if (packageMetadata.Description == null) + { + throw new EntityException(Strings.NuGetPackagePropertyMissing, "Description"); + } + else if (packageMetadata.Description != null && packageMetadata.Description.Length > 4000) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Description", "4000"); + } + if (packageMetadata.IconUrl != null && packageMetadata.IconUrl.AbsoluteUri.Length > 4000) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "IconUrl", "4000"); + } + if (packageMetadata.LicenseUrl != null && packageMetadata.LicenseUrl.AbsoluteUri.Length > 4000) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "LicenseUrl", "4000"); + } + if (packageMetadata.ProjectUrl != null && packageMetadata.ProjectUrl.AbsoluteUri.Length > 4000) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "ProjectUrl", "4000"); + } + if (packageMetadata.Summary != null && packageMetadata.Summary.Length > 4000) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Summary", "4000"); + } + if (packageMetadata.Tags != null && packageMetadata.Tags.Length > 4000) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Tags", "4000"); + } + if (packageMetadata.Title != null && packageMetadata.Title.Length > 256) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Title", "256"); + } + + if (packageMetadata.Version != null && packageMetadata.Version.ToFullString().Length > 64) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Version", "64"); + } + + if (packageMetadata.Language != null && packageMetadata.Language.Length > 20) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Language", "20"); + } + + // Validate dependencies + if (packageMetadata.GetDependencyGroups() != null) + { + var packageDependencies = packageMetadata.GetDependencyGroups().ToList(); + + foreach (var dependency in packageDependencies.SelectMany(s => s.Packages)) + { + // NuGet.Core compatibility - dependency package id can not be > 128 characters + if (dependency.Id != null && dependency.Id.Length > CoreConstants.MaxPackageIdLength) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Dependency.Id", CoreConstants.MaxPackageIdLength); + } + + // NuGet.Core compatibility - dependency versionspec can not be > 256 characters + if (dependency.VersionRange != null && dependency.VersionRange.ToString().Length > 256) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Dependency.VersionSpec", "256"); + } + } + + // NuGet.Core compatibility - flattened dependencies should be <= Int16.MaxValue + if (packageDependencies.Flatten().Length > Int16.MaxValue) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Dependencies", Int16.MaxValue); + } + } + + // Validate repository metadata + if (packageMetadata.RepositoryType != null && packageMetadata.RepositoryType.Length > 100) + { + throw new EntityException(Strings.NuGetPackagePropertyTooLong, "RepositoryType", "100"); + } + } } } \ No newline at end of file diff --git a/src/NuGetGallery/Infrastructure/HttpStatusCodeWithBodyResult.cs b/src/NuGetGallery/Infrastructure/HttpStatusCodeWithBodyResult.cs index 9fb65781db..78fe1199e0 100644 --- a/src/NuGetGallery/Infrastructure/HttpStatusCodeWithBodyResult.cs +++ b/src/NuGetGallery/Infrastructure/HttpStatusCodeWithBodyResult.cs @@ -1,5 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; using System.Net; using System.Web.Mvc; @@ -7,6 +10,8 @@ namespace NuGetGallery { public class HttpStatusCodeWithBodyResult : HttpStatusCodeResult { + private static readonly string[] LineEndings = new[] { "\n", "\r" }; + public string Body { get; private set; } public HttpStatusCodeWithBodyResult(HttpStatusCode statusCode, string statusDescription) @@ -15,7 +20,7 @@ public HttpStatusCodeWithBodyResult(HttpStatusCode statusCode, string statusDesc } public HttpStatusCodeWithBodyResult(HttpStatusCode statusCode, string statusDescription, string body) - : base((int)statusCode, statusDescription) + : base((int)statusCode, ConvertToSingleLine(statusDescription)) { Body = body; } @@ -26,5 +31,19 @@ public override void ExecuteResult(ControllerContext context) var response = context.RequestContext.HttpContext.Response; response.Write(Body); } + + private static string ConvertToSingleLine(string reasonPhrase) + { + if (reasonPhrase == null || LineEndings.All(x => !reasonPhrase.Contains(x))) + { + return reasonPhrase; + } + + var lines = reasonPhrase + .Split(LineEndings, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()); + + return string.Join(" ", lines); + } } } \ No newline at end of file diff --git a/src/NuGetGallery/Infrastructure/HttpStatusCodeWithServerWarningResult.cs b/src/NuGetGallery/Infrastructure/HttpStatusCodeWithServerWarningResult.cs index 7a1ddb60c5..40c4209a1f 100644 --- a/src/NuGetGallery/Infrastructure/HttpStatusCodeWithServerWarningResult.cs +++ b/src/NuGetGallery/Infrastructure/HttpStatusCodeWithServerWarningResult.cs @@ -1,29 +1,36 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Web.Mvc; -using NuGet.Protocol; namespace NuGetGallery { public class HttpStatusCodeWithServerWarningResult : HttpStatusCodeResult { - private readonly string _warningMessage; + public IReadOnlyList Warnings { get; } - public HttpStatusCodeWithServerWarningResult(HttpStatusCode statusCode, string warningMessage) + public HttpStatusCodeWithServerWarningResult(HttpStatusCode statusCode, IReadOnlyList warnings) : base((int)statusCode) { - _warningMessage = warningMessage; + Warnings = warnings ?? new string[0]; } public override void ExecuteResult(ControllerContext context) { var response = context.RequestContext.HttpContext.Response; - if (!string.IsNullOrEmpty(_warningMessage) && !response.HeadersWritten) + if (Warnings.Any() && !response.HeadersWritten) { - response.AppendHeader(ProtocolConstants.ServerWarningHeader, _warningMessage); + foreach (var warning in Warnings) + { + if (!string.IsNullOrWhiteSpace(warning)) + { + response.AppendHeader(Constants.WarningHeaderName, warning); + } + } } base.ExecuteResult(context); diff --git a/src/NuGetGallery/Migrations/201808010014291_AddRepositoryType.Designer.cs b/src/NuGetGallery/Migrations/201808010014291_AddRepositoryType.Designer.cs new file mode 100644 index 0000000000..48cd2eb3ce --- /dev/null +++ b/src/NuGetGallery/Migrations/201808010014291_AddRepositoryType.Designer.cs @@ -0,0 +1,29 @@ +// +namespace NuGetGallery.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class AddRepositoryType : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddRepositoryType)); + + string IMigrationMetadata.Id + { + get { return "201808010014291_AddRepositoryType"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201808010014291_AddRepositoryType.cs b/src/NuGetGallery/Migrations/201808010014291_AddRepositoryType.cs new file mode 100644 index 0000000000..a7224d2690 --- /dev/null +++ b/src/NuGetGallery/Migrations/201808010014291_AddRepositoryType.cs @@ -0,0 +1,18 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class AddRepositoryType : DbMigration + { + public override void Up() + { + AddColumn("dbo.Packages", "RepositoryType", c => c.String(maxLength: 100)); + } + + public override void Down() + { + DropColumn("dbo.Packages", "RepositoryType"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201808010014291_AddRepositoryType.resx b/src/NuGetGallery/Migrations/201808010014291_AddRepositoryType.resx new file mode 100644 index 0000000000..098426f8d5 --- /dev/null +++ b/src/NuGetGallery/Migrations/201808010014291_AddRepositoryType.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + H4sIAAAAAAAEAO19bW8cuXLu9wD5D4P5lAQby/JuTk4MK4FXtvYIsWzD0i7uN6E9Q0md7eme091jSyfIL7sf7k/KX7j93nypIovd7DftwIChaZJFsviwSBaLVf/7f//fm/943AWrbyxO/Cg8W5++eLlesXATbf3w/mx9SO/++c/r//j3v/+7N++3u8fVb3W+H/N8WckwOVs/pOn+9clJsnlgOy95sfM3cZREd+mLTbQ78bbRyauXL//t5PT0hGUk1hmt1erNl0OY+jtW/Mh+nkfhhu3TgxdcRVsWJNX3LOW6oLr66O1Ysvc27Gz98fALS3/xgoDFT+vV28D3sjZcs+BuvfLCMEq9NGvh618Tdp3GUXh/vc8+eMHN055l+e68IGFVy1+32amdePkq78RJW7AmtTkkabSzJHj6Y8WVE7l4J96uG65lfHuf8Td9yntd8O5sfc7i1L/zN16a9V+u7/V5EOd5Re6+4Mr8sBJTojj7VMPh5Yvi3w+r80OQHmJ2FrJDGnvBD6vPh6+Bv/lP9nQT/c7Cs/AQBHwzs4ZmacKH7NPnONpnNT99YXdV47NM69WJWPBELtmU4wuVvboM0x9frbMuBIH3NWANDjgOXKdZl35hIYuz3m4/e2nK4mwYL7es4KRSvVTZ9YN3evNw2H3dx36Y1vVmEMzm0Xp15T1+YOF9+nC2/imbOBf+I9vWH6qm/Br62azLyqTxgQFN1VdPqvrVv/xpiLqvD1//i210FWd/kio2sTiK07Equ0ySA4vH6dNIdb1/3PtxJV7Kqt5lUL/JxLBS8qP3zb8vsko0MrEac1IhWa++sKDImDz4+1IO82LjVi1wEUe7L1EgiiQl3+11dIg3WctuIkLmGy++Z6nYjTcnrQzUSkaJGlk6SuWOElIPP45VhHr1tHLW2xNBUS2M/lCArjFKAnSNfpt5CTY9T9C2GcygNBbOBbXSatpZzbXjBDPI953nB2+325gliYNdgGEGZmeE8M6Pd2w7br0ZEEIvX7LQiv700xC7nLKXQRB9Z9u68p+jbHJ4oTWty+QdC1jan9D7MM98lUHev/A2GXzeHtKHHC0bYZ3vSv5jlM31p8/e5nfvnn0+JA8OWpyz8byETtHGYlZqRjOfNL1R89lLku9RvP3CEpZOVGO7/XpXrDOGLZi8RMUsFwq/phvbkh+8JL3ImJ71J7r3ww4UuNLn0aE9YvRedmNWiLasFLzsNum35WLBrbRikrq4Sum26+lVJssyzMPtKii2OaS1tE5QdrBiKrRl1bXoU3zvhf7fiqQr/75E0hf21wNLUryV+lJSy3WZ4d5oS7jsoWYgDMUs+ogMmb5In17adY7UJ3pXnPSA2HR9mwmNtW9lLnjjb2zbaA7hpirZbj99zzZgQoOxPIrUQTPaip+cnIa1VbLE0uIrzMoyyZaF12xziLON5+co20D7mgYJGZ8UYY1kAQ8ZUL4uhyGjkqLjwQhkr1t9RLt20RW1TZHjIWkIzYGsbc3Kjn4A+c0LDrpaB9Lu3rDQC9PL7eAnu3cs2cT+nj+3DFZXi5aBK6q27eqOuwPEz6Pd/lAQI+h5WWK7y2/PJzf+5veEmyE5ns1njGxmAf0krzWb7CcsqYukW14qtkJaTlPWFCWDM82axelE0WUjp5dOy0XRQ/JKUeQ+LhLGWVuNT++lotgAqlTmcaNWqbPebgxCl1qbA01AfwkgTzZURJDPO/kQatpVpctNKj4jrSnTek37QpFWaRPIs58vdBQC+snxc7TV7Q/czMAcNPCW1CAhWKuG66qKHUvK3ESOr+tqpoFzkof4bZuznZ1gBmWewrlsRUfZ9T5KRPAiTlExdpIget0gUaLoiEwrYT6y73zjOgkciEaf3cDb7c4PnRxBba5QXC3fioatGGX4NqPz5G5YNI7GF5xfNCUxtUcSiMZQ1lv3qp8kuWK7r9m0zrpjKz6UktPKDIPAwORM2YtOEsapeBGb0ofSZVLMw75bjGcrpRpOD3F1Y5y9nUWRUQ7xGW7LHkJt12RTWq/L62z/0mm/4lDWrH72ElZBu0bAmjgoMpuHGRZ5g0sZQvpteSHFqU03tZnU2J6XLO3C02GtWt4iNZcVauLlyYBgN5fZRvE9jtw2zzKCpO4popULcfJkU0oe1VX9rgdPX/15iO3VZXL94MXCCPe1jfwcszv/0d3crg06XFt/yIuU0UyE2uDK4PILu/eTNNbY2ag1wkV1nYFKELoGFuu1IgMUydICKHuUF6Yr8PGFxbvoexhE3raLFakiJrKx9O98F8bYH6LN7x3odBE4AFABkYPnUmamJmtHsUNveFtA3/Q6H6nxTWZ7g7+/HvxsJbr2763Yr5TTd0bKTuqTXGY0W0ZHy4HZzlG7ivRZDmyXgKPYN71LUAbKwV3E/in27x+Gv0qcwpyKZpfmpn/ZpGZewj5GaWu/NVhlDlfj9485T72gQtevcTB46//iJQ9vg/so9tOHnW4rQ3tvb65sfNPLy00UjsHKbAeUWy33Niio6VyneSFn1Nguk9+v3DbOEdHC7nG/HV0m5fW+3/pgtYaS/oaFyTgz1N+yqrovbB/FvfH1wQvvD8WeAJ+JLmZ7sRngHzuOMqaV7LzwM3T6f2OcQM7tby1Zlf3MbXvGGOZ8bBM/6/vTuLUZjPBPXzoS/F+Yt71i2TAUK5yCYfPtY26MXc2Dt5vczZEX9leTXR92Oy8e3kDtxrsffidy46eBdlY7scWvTgGjP9z4GMU7L8im9NZZC0zGgfni9oF9Y4D9Mml5KA52gw+7sDiMITw+ZGceFwqjzzGLy516X1oXQb5MhGybP9yP4uF53lT4ju1ZmB0ay4d9Y9VaLXJ5dcPXeuWH54GfnYvN8+4nF/OuiyGvI0cQFV+zzW16SLg28FqvMtH+dM80fnvIVn01vHGl3G2TR1HA1UmYsq1Jt1WsmRz/1PSFfGrzeFc+WBP5PLbNFOeqpp1iRrWhfDraUiGTbVMFma5trJDztlH3KY0G8ykqSW1m2+v2qtxfsuUiig08LzM9aTog5cCaLmfr2Gjx/mwoTT7SB73a37In1SKhgbuYUYU7n47CXchk/UT+sM+RxrYXcfb7exT/rm1xk0sDFyUPxmw1oy2jr592X6OgHXwNq+WsKrPFHCi7pWy2DEctw2v68nMP/jvaqP6vMCtK5UJke2lRljpeXZD2OP2tpD2t+7COe0+TNOu79UGEgLI16oPeZuV/skVwW/KI4nFQ7MR+g6Qwud6zjaYuN8qZEt7Nkua+vl7z02pfjc1UcPPdZ7pKynW7GSsUPk7acSatzoufrQI4N9bnFMndGkTRvQ10u3ke7XbMib9yy7Opxam0LWE4ltYZsR0mkrujidZwx2pS8+VziQMZ1lF6HeVWr62uKzNPjeGapRrIfsKRFEHK9OwD2UpRYwvZqtgRsuMstV2U8flCnKTebt97dR7nRnOsS6PxDPzGNLYby7xqRJObZ2z2MaJh5NFoY0SbyBlZeTq1wLIzDXRnkObwKGF/tYUcH7AbMFdK/5o+ovvnk01NdHYTUJqn2W0R85Tj/nDJtwDWJmdT3DZYXZti2kzwbrXPhOF0v3azpil4nDoL1rF3uxpHpDl+h94JocKdNd3VLl/qiM1RNezjWu7j2zODWaSTneYf4CXUEHaNX6Lv0vL8sx8Wp7M8GvEhjvPb7KuizUWHhH4ielOXIOu1xFub62DLPGLV00mM5pTpDmCyzEehOStLlvxs1DEqDegTSQxY0wlSasgYq5CSYtEj3Lqo1N2eu4aLUe0yuojj0C1DTFTXwZqgwEe6oE6dpvP5oUDmBcvXQ2rso7bMcQLPar248sJs04AsGdyw3bYZuRgqQLoaSAXK5NTvDV8DtJ2C0rXN1Fo+204U2yOpWOw4XQxnyXbYei97wAOJ/t7kD/mY58F+g+Cpamzvx43hJjhs+9MZ6KYOf8nFrxyu5rESVVY32V2/0xHnKuRhCWi/JjcmlXRFesmoSq5cszQtRp8oo8RiRxllmGfsMQWdbDiac7Za/ML3nG0UAKDscdzHX0/yQBVwuLJuPvcz/Lmhx4cROM+AOooFyCBBA/BYYsAcuG0LKPo7MB92HQJnduCWltwTw+pFLELqH3Ud09utCgAmd1Mpp++ilJ3UPbmMC1uF0gmB/ZuvvNRRUJMcPHwK7QUJTOnnJwfy2UtGCfgoa+H8+9DLcTBC1RpnChUbdbO6zHTL5VVmspwFm71KviH88laVaC58pByG1rrRUrzdbHLfkpbiRSh1FC8ji5eK+71ljDtp5VJqWAqJihngzBNgeisXaOefLp8yC7WZ7d3G6CQdWJUo6ZAstFYjkg4XHoedujOp7/Yvk4vAu0+aYbXbqZRUXIZ7+jXcsjh4ymDII1sclzKCSi0Kv3l+ULpFLW6DztYvlYEUCjSOoarsp/rsWS5/65WKlqqE+txFKHGRNYht63L5dqAq96M6YOXQ8B/fJkm08YuC9TSVgzSLdb8PtytTxObWtKMKan6VDYa/z9ifAeZs/U9KhzREG91dS5QPIC1SPl3La8GnsByAVRkcOzuDesnG26oiJOPOVvySLR8sLuvJDq75GcQPU3Wt8cONv/cCQ/ulctRVKm9YU4WcUj9ETw0DQqlbCpWutqKpTGKbiUtvTjiIUZBXnrj0+JCOWb3xJh7BOHrlrapI7uWLF+ok7gEWofLxcCLwkFJtqwOaBB0croGofO2ISvkglOACRAcVmTARLtPLJKThY0ANGQxK1Y2VyiRog8OPY9AwxCJvAcJntMGePoz5aAJL24wx8KRlNKUBdZnpkCUGjMcGHI4PP6aowdoCgM4O1Z2QB/JjDMSBnadUfBPNAGfaUOTa8SZFGTfiEZdoFsHP20r0kdWHknpkfoyGRwrfKI2RQtrPGKwGSaktOgfxqW9gT6i7lLEkTs4C6FbSuIgzPK1A1gX/xrBDigQOg8ZGGJPiwLfVtBG0hwUkofdjIJHAHdL5eRaSFowwbYGLDrgbRKrqI2FDWB0PpFOjc4GwRCM4Y+NvDufcYgAIX04/ehvDQRt2FRJtC55AFjAGruBFIL6A1jx0zmgqG4s3jcdlQxvVyBMKF0aSW3KDAE5VWWof5INILYQvYwgshAOUqvnX5ZOIqbrtfCAR00iDUUWM8CNMPDAWCa/m5ho52OFY05YxwQQwmVK9HBRnUlAJ3qlNYw+HgJlaqsEOtJVW8Q7qBxVvEJfGhCXEjyUJOsSrqwEGJp/KCh6klw/WktDkUxar0Pl+BPbabNV6xSXOwNySPeuMLEP6TG9tR0ac5toBXNJ0lz0FGoCEug1UINT4WrYGKxp1azkwRbowIkCRgVoiNLX38EDeQUE5zS28phETYGpZdh3SK9nSd6JpV8tnns9eW2gVLhbzLwPvsiH+jLnLhjixJNmm+kw0jL3GgaIy/pyfSGs5p4lhaES/Ku86TFPJ/5lpSmCxC6eeqpiTNs5wVXRNOeh0hbk05oSF+bGkKUvah5g2IF10fpNuOabaayxzkwGH5jUMsD70rt3lzbBSzRBRuAPa+0BTy7cRoarlioWEUzxCzAbJVViNbf6ez/YuUio70qWkXOvgt5PqnTHQLJu7baj4CDfdYLX4DLcYM0vbQMjZIdYNo+dDcdRlt6N0fplcJ87+YYihA2NITcNgzX+tV8wU7Q1W7e1ULcyq52DHZ+zxWEjTcobSiI/se8m0GQHOAmmzM4WexmQP58skQLRC4CzgV3j81sNOdAput9PCqAFYyRPcbjWK9YezCzH0U849hxmmtAnZkWgMhFzONIxFo002jB/z313wZk1kXOoKgc+hcRgMhFJtCycDK4VvY2CWwh1KO2Zh3Ab6B0dxoXUWziGW9xJs8YJf52Z8MGUAwQGwocGaohrudFAyW7gSnlAT2W1yk7s2yhQnD+ziVIagh27KjNQpuzUTfqglSutIvPtk6wNekE0jwhVkBmklkjzwT6nLhj3zGtTJBje9iiwU3Wdba6/1jn5nr1ek9GLEKxnt4BG1PhM7P4J6YrORIJYfCdHPaV9h2b+pYP+cdhhQ/2SP3TZwRN13DzoJMAfgi5TuSGemQjsyopTmQDEPpkS54tfVgDHcySvwCqv052yNY9RB7FKwi3VgRLxi40RpguiTeQbopBo3oc7LidgEDdqMtXQ2SbLgh9aLNNZemkvpttWSH3b6vCW5o5795KX0YowZTBk2i2nMe2qfZC5jHsSt0ASvNG4Ra7PSONG7GFoxGdhmvly8LzzDZ2XSrETjqLz46rMk/84eoahi2UhW/t+TytG/DI+c7jVL1QuOZL163/ijh+61FKSJpJTLNZWccglFIInRMRZuHeeCPeMcGBsIFc6eIRqVq2xDcd61J0RFdP1pIKZ38KcS1/vAM1SmGDdBNQAWUGSyenpGQoq1JUQPsOc0kAUNTVXCoG6FRlpDjkqi9tqDEqp939DICY4zUJq80wkaXeFdt4ay9CDfiriZLJVg+SBTy4LmxSuNYvk6EqVWviykkWpekmnocS/VTEJNfBYGCDfxOZVpKhbmTMDsK4yLCGJesGCFRwAyNTatAe2tCbgI8HdiNFIajsn3WAaCYnRXiKAcNpaGE16No4GKqIijSol8G6QVEOUu1UBO2JFB5KRNr0SO2zNJqzQfdGXFZeMXbDQwi7B7RkOzNH1pNwbKflBDpt59c2SEjYp87hP7SuZDFXQRYwGg4QWaLepyu3Rc1MtyFKqdXe/eyiEtgB5ro14IbcbiXtDGSktqQBbAcRYARhACMgh90Idk4Hoi7XA1bNEHYRiAOWKoAIApmlgCQsvhaAKmFmtIAJ3W8rFr5/UnAIQhdKf3ag9Jbu/tGUdydM+RNZyUhmcuDjcLL+12fOgDTJp79REZrHP2DXCW7Btc6DrFO7gVLyl+wDmC0BF7CM5RWWbJK3dMMnDHBVtQB8kAa2jOlIVOGd0pcx2DNBcafhkdKA+wckLmDji/NLnxbuGFIJ7BahkN1zTkh+db41YZZxbseRnqguJ7WWULiRWKy2SVTtNsZ4wQFL44M1B/wGBHII/AXZkC+QDmt/yCTtsZW0SNG84X3KUt2BnQqW1XzoC+aFViYldccQhxnIqziuJpFeqmwdeq2l9ZqWnmpMG7KlrFYOxslFREbtb5LXtaA2k4XtY14Kx0yEPZuSfOPa0bUKhXmCNQtT+cktrMLMz35yhswhQRUDZyTzAlRB/uDK2FqLkvXgrgMl/IZxbTfPbeMl8ghsOl6oMr1qj++3D2GHz9Qb3Cvf2pPePvWcz8wv37DTrFZLd+GjDpHACCCEBcAHYFFOK5j1cnS31xxiSDACJKHpPIobFhJBkDe0wjntjoINH7WHN9atOBxz1stK66iJzUevgy9hfz8eWar5hXrwHgSfTnRVLFgCVttCcQgUHUNGBFOIi1o9lRRw35/UIU00YXYYrWWOckTAKResFv0Ejr/IINdVMEeqGiqPG7aO8dK+1HVzMDbpQorLLikSvmjKBe5tz6YGwAnP6ozRbd/th3W3T0w0uzsm1uOqpaeyJ91vtcUZuPulux5wTqF0Ui5VjDp/U4AxkokD3UiCYGFB81RD0mnfBILIRcnkCsM7pGEXumc47Cs0owG9OxSucOZQhzF7MjFJxLulLGLmoKazhIOiFZuEwZfssE+t4wII9wbNJ66eiLPN3xyDAQ3U9JsBMI/HhEcBoBnVf0biNUPEjWj+YDkd5RxHAHdaMjAiIr7WYzkcJgDJ52ckMtkp+9E9mufS1vZAP2Xt41u7EX8sPBWnkCh/NT/1oO6hv6Xg66k6xsi80cQ1/IDc4lgn5O+zxY0xuCTs6eRSPp4bQvgQFW0V8OC30jvR3meihbrWvYRnotPAC8sHeoVKZpJ6Hp0apDVrmZhPmr6bxw84yySXtzcr15YDuv+vDmJMuyYfv04AVX0ZYFSZ1w5e33+euMtmT1ZXWdK/iy/dU/X69Xj7sgTM7WD2m6f31ykhSkkxc7fxNHSXSXvthEuxNvG528evny305OT092JY2TjTCf5UefTU1pFGczS0rNqs5aeuHHSfrOS72vXv7M6Xy7U7IVH7hHoyL3GvbWtQHvQtXBq59r1IXyvytHT4dfWFo9XHmhselpOXmRdW7HwrToJ4MPrGrprPz1xgu8uH6syz8WPo+Cwy7UvB7Gy18/eKc3D4fd133s59KGJyWn0aliFLtRuz58/S+2kRtXf7TpaxSnMDEhhU7xMkkO+R6Kp1V/s2wXREpIoNN7/7j36001T47/rlJ7cyIBVJ4GJ8o8kASTPLdIM4+gASLOPvm1tf0MNFIYahbKfnd5UiafvDjVxhE1Tw71Tj0pAuyG/TLJ//509w/K+P9jx1Efb6iLRy9vt9uYJYk0PYUUi2EON1F458c7tsWJo5ns4BRmf6l4Kr/a8iAIou/5Q1SVB3WKjSCudlKyLG4+W7Qu9L4GrPAOcuFtMrDkNsL547YNJFNNmen1foyyef5UnSs+H5IHuTdgBkuun5coKBp3E/3O5O4geei1fPaS5HsUb/PL4RSoAUrvQ71d1N4VFtf6uuTcFiI6Zrli8dd0I4ln7jud2gcvSS8yVrPth+jeDxWqUDqdOlfyvDxE8rTVVPdLgc26zd9bdhDf2rtWnEdUOT7Rosg/vO1+HkFfflOOI5rCQy2OtjsW9PxR+JkQTh6A5wkdhd+84CCRqD5ZtIKFXpheSlK8/Uqn9I4lm9jfq8uPkGCxXBYjm0qMbr9ai0VQJlqfWZi8I6o/djn73Pib3yF6fKKdwM6AuFXFdPl1NoKj8lXQWWaUTp7sxQVSbrATUyOd1AOTmESn2bpv5cnhTl1xSq40FtVOuPSWKFKTkmaDP/FRf2cYCl7C7NGoLz4UKH+OthKB8ovFzq3yEaG0REiwACJTlHpsGuXbTQT2i/s8Gwjb+ivosM81OZmw3PeayaGHTvadJ6SMD5RuIb+2u+zwAo27mGIh9/Xn115H14p56jlSSJgNTAEj187YVL0a2gOSQANdeXUQ7IE/MfiqhGtNXEyc4mVSIFdWM1UfjzjuhWMnAO6F3Okhi+B1FLBOBADg2UlnHKjuT+3hQKAx1A7ShdrhMrl+8GLBA6w4/kqyDe3PMbvzH2WS9dfZYIr4ZoqIKoLRIQFXJCpDIUtWQ1kqoKLvYRB5W0CdLCXZYOk3Fvt3vnpp0363ofYh2vyu0qq/zg2Z/dHYHYHjoQ6AvEISy2Ozmdo/xf79QypvoprP4ys0Xatsv7CAeQn7GKWyllRMmXZOv39M8+vg+nH7r3Ega2DVdDr1v3jJw9vgPor99GEnEpaS7GiqpKwkzyYKlY42H60kWG4AkyoSrPpqT+k6zS+nYXp1WgeqbJcJ6FcI2Tqxa2v11MUslur7/Vad2UKCHb33Wx8kV3+3oFa6+FFAxH+3QLS/lf3LC/BWk236Hd4fCnclYq/rrxYrw+Fr4Ks2Dtxn61Xmws+w4f9NMQKQEi3oxlGu5lTGhf9uI8H3UeLnDngUglJSF5rqlaecZiUTvzBve8Uuw1JcKwKyTLZVSuSXeRX03m5y+2MvlE8nmmw2OuvdzoulbUbz0UJn7d1La235xYKCn8oCuPpkcSZkcaLsIpqPNvZF8c4LsimwBSkCyTb3DLlI/sC+MfU+Tk6zlovFSRWUjFWKNcVS8GHylku1oZyoq0H1zfJ8HZcbOuCM3aRY3CgFXpqykG0bT5fCtZKS2oGy6A8RJK9zmUiqQ3TDBdah89Slq+PKD88DPzsugdNCTR3fnAW0bexg2VhxKNtFpYcEO4dxqWMZME97Esfd0tqdx6sIRJ1P5Vj5gc/mGA5sr1o82TK4/DK30Tb4l7Ubci5AVOdh19FYxtD3Uy1WcvV6zzbgFqdMsNm05Q/puMhQ4v5NSpwbPCX/sH0BKh70OmPUQGYZMHVrwX2d36Uqx5f2q90ZDtiMdtqFnke7nWLy03ycKdidwbw3wMeD9lKWy9a/cN8xqiMJdh4jlMAyxI8z23Y/O/Om3m4vqxWaz+OqJ8BzZYfjpMtLHOf28o5U/G4VvUtQT7q+tPpjqhH7X1INcXU2lNrd9WVNl6uGaVddzBO+3YpbqP07L7dw6WWstfTdlSul+7R44Xz49wVNezbujBwNiaHgs9BDvzFWAfXRlhC82n7gDOWXMeldWe64vJmG18Yui+I8bV+Qq4ROdwhfou+g1OW/z2bmYq6tqWbKeZx4+3kKF/vD6ihAP/udx0R1wt/RmYyJyFDj5UrD0H/3dH34ihzAxZRhDOOnctnA+zLujEKOShenDbrSf1g5ITuF7js43TdaJgJDDRGHC/V+Xkqbm3X320Ma5S/hsrTgqWqsom0Eclio98JNcNgqbwearzY2VoqmC1VxTTQZKixn33M/yT0mg0iow2QwERhMXrHHVGMgCyTPZvBAL9l9j/w8te6nfj2VgY+QA4ug2k8++Dy+g38QyU+5QhZKt7lJaR8P505z5QsVOdW63Ut5WSw5/u47T0o6fSx94PJDzY3KQvCTcgfWfLam9fOT0igxxQZLXqIc9atvFicN/z700kMs22G0n2eDRslJeGc0CnQ6oNFQfkForHqCQZJPnhLpc8So6ETe8B7/tliFsk0s9dF9U8D4tB4LuqgIC7iG2hm/yj8ahhW64NDmzGxa1KOxlY//ro1FVTl4++RoAdZYgALYGNCgK0J4Ek9EBF5LX0xQt4uWqNA0eLm4EM5Jt409n41VKleKYn2qN9jD+Q7X2V9+5OQcwgNpZk+EVHQmAgkQ7J54B90WsL1qxjmMRrDvKS9uHaJAbWNPnNq3LTsmbv18VFeXycdDEJyt77xAfpSm6fogK48SndxiCVLKEtei046LERLUfAGrEhImfUnLEzX8OnlDCxc3bm8JLn9IG0io+oXsfcGmuxG5PSZDb4Tx4a81F8p1FuieGLrNB1iqRskecSaaWtRzIHMiY48cHGzZfB3LZdZcuxKPL7rQzF2ZabrIsxxlXbDnJa0FcExH095ByU70pYWvsUikyp4ysKLqcC+ABL+c59YT774ZN0q8RDlLo4SrvjS/m3iJVaxCIYhiwZk8JOKqWQSB4IVllvUqY8I3f5sHLrx+SlK2e5FneHH916D0OtBmyKagf8eSMo7N2frVy9NX69XbwPeSMsJlFZbx9eaQ5LfhYRilVfxLQpzG0x/zOI1suzuRi9tHe8ypJMlWCBvCabdr6cKQOGtvsrGUEVEj5Qu7W6GoenMil3wDYTNvw9m6CEFYTOlfWDbypVFGmnu74eNg5OjLPU41CDzR0pcDJpZVffPizYMXr1dX3uMHFt6nD2frn15aE9cQlsfs9WW4ZY9n6/8uir5eXf6fWz7E3m1L6odsiv0a+n89ZLlu4gNb/Y/QzFf/8id7JtS+4ctGhlUr/2HnPf4jTyyNVestlaF8XEYHBOtgiq7a5pAeH52xJJc/dEn9HXuVCwG28ZNiNv+rnjB/MaCdgNpghwuehLK7Ea4qwjyRw1HeiuSq/csPq09xJpJfr15CE8iywc2eaNCWnlq31ApJzwY+YnREcV7rZCNliqNBGB1X04Rk1ND9008dWVMHZyxpf/VTazpcVMbuRIyBF7uTBuMs9mgpElNRMzg/vrIdcyis4sAVyLEUxUXLlj7vhaMfJSh2Yj+KasRETgC5kaFYtL0FS9Ie65qwxHZYUkuHl26lX/XcgC6qaS1togG6XQMERwduSbeAcEu3eaonT1YEmOfRbn8oCtgK5DqsYD+poEQTrJeIe0kuUIVWGUnQplFk8QJE51uwZJEi/NnJF6FwLynT2ubataAuh1VOOn06PBRLoQQNFPuscnhYvgWjsYzx52AchBh/dpDiivZCFWs3N122ui5RyUUGtONFU9BicpMRTI+6R0O0Ob6eGeAQDTuWqRR6CUYxuJ9dU/iyvRphPHFRJJtKVnjpgK/VfTBmCKRHA5YxBBkMRjTOmBmGPTHoEoBiR6znQlO2VyOaQGvd5elzwPBY4H0eyO0M24kwSwaEIajegvd/ZsXA6as/dxgJJUxfH41rHaDP+bgaLdwWPLIGtUyXYZUia1H1iNCQtlHy+gCjjo83FDCeDRgwm0M7MQ1T6bnRbTykOjh2jaaF02goOzVc9CjqgKCzqQoFvXPQPsntl05QvexC27muu3GY68SQow6L10v4CaHwHFCqI9O5apQDgoLT1KEnNR//ructHucS2cVkUaPe9WFpHe9ON0OsJx3nYXDocVK882J3FzRqnMNpJ5JccDftlKLxZvD0ZRdhWcfDAzFFayHqsLqPOrbyVe1CHVu4rHZBqHQw7/busHEQ6fbOF4iJR66Apm8XQ+OBewwLaVkcFp2MkhoKzwnRpKfVjxgFrzsdNe6di/sbONqdS8pijDsHlNXwdhqA/2QN8GFsQGgGEL1NzNSAeN3PAP2sUsXSfdgCPJvodIBtCdBbY6s6gALiLV+B0J3nvW9FPNUmlCA0bIcNC2h3HLoeenkrTaTFjqmMrud2M6b42+9D3hZ9mlB1RwD2UDKSzHNpp5ImRl735RTdl/ZWUjXB8iYA7bOBK7TSWF6d2LIQjA63YBZOPuOn27pzoex6ippBdA0uj4xu725cX6m4VNg71ucuQOXo+Drqj6MhtL18mvqyzK0yXXtfY7uI4DcKThffQqt+XHmnPaeTteIDnPkRP3wLRoHTQywKsh4auIF0b5pQcAsezcnn9GhmNSSRT7pzcLOcLc5KxYm6n49ZV1KJo+/f6i/9xpxucqv4l1vwDB5aeW0K3LZg1k34ENuoCOroYajT4+bu1twugYaGaFswwoaenLqwaQtmm+yw0fJSWCjda5rO1rYZjuzWw36kienWnUZXlQ4Z7bq4aAtGOxBXbfBzKR6UbMGMnO1sFaKh2bWGK9qrCVDkNLuWqBScPUMuw6050gUP/IBTE7NswVOHizbVU7UqBoOygxhftifay3hpBkjRdtVtOCp7cmRYaYKPHWHFwNBlnbDVEugFsFmgnI7MHhsGQmQyGkCJYRXUgohvdDO64RptVz2VxgzNBxy/VO46xuT4Ew5Heba7vtmONDluGHVi47G+1Nx4yC3KhG5qsp3FVcFeA8o33a7+tuSIE/j0OIOf3Qy2D6Q08OJMw8tC1+qZANPqrqbr5RYegQZACxydyDzKE964NG22xFRZbIjR0gZHog2aMbKRQyk9G6X8/KQy7+gqmezwTuCBm/5z8YPqk2juRvcW9dX9Ptyu8okkuPOump6H6HnBfy5c1+8Df5PVfLZWo0x+CssD9Kr0y5rR9JKNt1Vhk4dSwtpR+v3lm1B9EWv/J4VoNmYsLpt6ng12Js99NQb559gPN/7eC9ReS1mpaMi701CVU+qHZanQOUpNksNgtc6GtMRaExuEIFMU7BQ6ZRQ2hXzkR6v8IA7WyxcvTpXxmmrIoXB3Ew92q/ifZJxbrN0Cy53lWA8kFqzF09LQYimJNGEJR0AM7xW78eQ8qIwQ/HDztMSEZWMAdzaOVCj4354ECUVg1arJ8gZrHpLjiByoQs5X+XS4sXBNbo0lXJRoa+UJ6zMSxNe8MUPnPtIAsyf4uYBpnpJpECAuTXL1RqHoun8S/PF9qNxaG4Enaiaw0U5sBJvqhZ8nC6QOghyNymUgCBmiDyC1zkN0AdCZCDMIWJ4hShYED+XipFTHwKHYuWFVfdfzowukjrLgwRYr2pZBy7EjCBr8+w8EQ4LVDlIz8SZwUliWb1BnuduaF/hG2191x9u0eyrgbpYo+6CLd36YwfRRIIjZcxlaNyAMjUYKA6GSZNmG1E02hJkYpjOWhfMD4mjysA/yZiERb2tvTobBhQZzXHhV3lCBdtQpQ8q0MdEE+X1FauT9OkwKI85DL24UwOURrt3471b3OBbgdHXFj/VzWEjQbvmZ6GN5UkAIPsBnL1w4n71AW/jU5yJkMC/FSK1zETSCTXttb03dxIOvb/mBljKMiUCdsb6ppXL2ITGq8S88LGDJrxmQZuieL0wPYMP2ngyII2g1oF0SXPH3M1PiFfZAMsMFvZ+oX+6ybi+e57KyVw6slwOx2uM20JAm6bnACvQuvhBADW5NODUextZ22YBhHkouzk3vAg6jef1QK8rvz0WkFL1ZkjxpfOG6XKJookVxw8uT5BKfCzYQd8p6gNxOjpBbwXnwnCWN6OVYeCwjpiweURp/zno4TY+msTYuI6rPx96qLGePItz5UiXIfK0SRsTUxFYHz8PKoIquss19TT1Hq5hTijXC6fNEqMFhzHOB7PIsZCYF5dQ2MjQUTrs2q5apQGeei/m0SabTCjxn4+qucnzWttYgomnXkTNc/peE6qm2Bc5wPastghrAZL7+J4BgK3JL5ORl7wFM0WWQamf2qtzV+9+5P7ocDRbd3lp+ZN/LgjOCxjzfhE/1HnMCAJGqmwVs8pYbzwxTr075X0r95cdlIwb2nYlUNoP1p0SLYT+ujBY0Us8ILeQBHBUtecaJ0ZL/xz0DmOeqJDVSaYbmPcgSpY3tk5FphQ7XWjqaOrzsebbYGvuFUBeIzeKlEOcq+PbKC717wr0THzxSABr/fRwXkpJXZ6Q1Az5yRQNpDgQ0rR9rDGgmx9WTAW3Gt0XTQ2s8P6UdMDXx+iiEYoW05wvUmkvxZQHAPU8TDl1cXaTKWem8eclmtBma3do5GdwmWjltYDaLhbNqMB9EtglUOsuVE2gwJGHF9GWvoFCXKPUKsWpng67nsZzOB4YTW55Zo3JWqysEUCk68lEKauE3eykIhcueEmvl6LehY+eMryqGMegvpUx5FpiCQjUjNYrRgmeAI+o7AtuhnN27EvuxmsD6vxqU6cAhBB+XImXPUtKI0dL5Rkgpy5Y0mqDwSI1AmPT5IKr/soWLliMi9IiYcu0RvM/nI9MSLMI6FfeJtzeRW8MpHCsdHdwvPPhKL//3zlHyvogsmZVJsxIsrrVb0ZZd+HGSvvNS76unxCMvS12zVL2vXK/eN8EqoVvm680D23ln6+3XKBvyMualcIet4kWsS7klVupTckB1KnfnlHqRyvAazGT5mHYq57hEkHFNurmiKsqiUkf1HSJfJJkpi6G1lArEZKgePoe5On18HKV6fXaoOfrYQqbmAUbASpuAPFBDlGw2tWurNdVnrgh4FqXUB+SBqlWymWsHdYdK/WAuqAXQGxtqG/B6tXWR6dfek7Fa6nRNXbUPaWKNvDtVrFY+j6ZmwcsssXrJ8xvWAimbphFCTttmGBtAqZpcaeOICqu0yaCptMxjwfHScRFWZZmqqa9020Ssi/OGg1XIZdHU2uQiLH2izxR1CRTTwaVQ9BxjlI6Fba0qEIvPoAzMzYhJexD54RG4I5EzYfsTIR8FMcJltLpf4VPBDUubgVwXPm5yBk2N5JH7xQsCFucfcq0zUKmcAapUzEOeG6LGH5seYi7NDOEzWgj/8nyOC/4yXSv08yzmGiWNgFKjlA7VKGTpsGlBNvVIPtLmpYwH0mUHgzQGzUncydg1SOsDmLQW2y+PZTnbtRJwxEDl3SmZeZITC3uE0XaqtGIk/NltYtuHIOA6gi9YdarVkoEgXMlhWDpqI1Gro2JiOBsm5sOgWiGnVZFO2rf82Z3Lxp+6b/Ew8Lw1lqIhyKoSPyuKIs6THH/sLwpWX2Q1v9gVcjfLa3a0h8AtPKBE5FpXfpikQy1Hy5fpQKfkLC47Zj3OHbrIa1pu63jyUEfhjC67C2mNipJiQu8uF0rtVrmkdlXMsNgu6vVcSLdt4jH3YgVFaVdQ0mccnlU4RCxCjj8TZukiXQNcIgfG1t0CIb1Tll6BBqpzLegAqUOwhsqT8ZiBcMFB902RigFOWAU3FjqEqpqLfgGpGuZoT4AwPWi6umRZ9dLJhmXQ46ieMmc6xgBHHTOazIXwrmqOZEVHwXQN6wxHeYzm8OzDkWUu5BJbs2BQE0kTZwccbBPqCtR0CgvEyxy+dJ3irLvCDTjeZc3TZOwBOX844b+bu09hW48ui7dNeJ/x2Icux1q9RuMp8KmuGKAPvoczxCJoH9RR8MKO76uUwcw6ghISpS9nH5i5RhlrEVBOwwpNt58HQ5uJRWQkYt/rbvr2Q3V3tshxtnCGaCNyuWSFdCvNF2+SXHcfU0xB2QbYq4zZZTH8kWbdEvINNtq8NQBftvzuqtNqtB6844bIPi47r5gn8AS4RGdjLxsW4KOvC17jhAWgmUSpYRdTnHXeMMkHm93D7kPhUCHEUxh9fF2fX0dkChLNgsgiSiyMKQ78p5QDrWq6PgxL7c7+lFgNwygBRmOaXawAkjZOR2AidaYJ6rQCYzHbRvWpIzDe1J8rw1WjP3RxxbK6nOy4oWJTXk52f7VndaM3yEXe+PdSSr9ovR+q28PdQBVdKAyQNCJbzeQa5LztVFOq/Oiyi7igVDNphJfcWKih43dRfYqFdFPJ6Ho0MTW3ktbf1kjnxhUyPCK7fXWqtx+ZKRqPoxBPqA5KRZaoFuwlS/jvOjMt2JxRpuHo8krnG5PKkgE285MwQVKDiTsolBe6UuPtG+G3DDy/3J24Qa+DBrAQ1A6OJs4oLKgowZ7x8DMxwZOem1Mw9NKDx5WYPgg7iLOIWnR07csMWCg7GyOyT+ujbMn4UtyM4AzReyRxwwTxYZF4+V6mOO44QderdfzkohMjana17ooAFtDdG/Ucf/BhWVFUShmGBTDwSa54FtPxjr5kTBayHb3S9GRbR6tbDdtyr1k5kcaxSpP25qR8ZVR9yH6mUZzNx6toy4Kk+Prm5Es2VP6Olb/esaTQytclMpohK5x2tUTrPJfhXVQ7k5FaVGepkxuHEam39VLvbX6Q8zZplrxhSVI8bf3NCw4sf63wlW0vw0+HdH9Isy6z3ddAUNjlfml09b85Udr85tO+4KmLLmTN9LMusE/hzwc/2DbtvvAC2V4FI5E7vPmFZd/Lscy3Iez+qaH0MQqJhCr2NX56bthuH+SH9E/htfeNdWlbBt8P7N7bPGXfv/nbHMsYEfNAiGx/88737mNvl1Q02vLZzwzD293jv/9/sNOEf4vDAgA= + + + dbo + + \ No newline at end of file diff --git a/src/NuGetGallery/Migrations/201808032317064_FixSymbolCreatedColumnEFIssue.Designer.cs b/src/NuGetGallery/Migrations/201808032317064_FixSymbolCreatedColumnEFIssue.Designer.cs new file mode 100644 index 0000000000..0d6c32b413 --- /dev/null +++ b/src/NuGetGallery/Migrations/201808032317064_FixSymbolCreatedColumnEFIssue.Designer.cs @@ -0,0 +1,29 @@ +// +namespace NuGetGallery.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class FixSymbolCreatedColumnEFIssue : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(FixSymbolCreatedColumnEFIssue)); + + string IMigrationMetadata.Id + { + get { return "201808032317064_FixSymbolCreatedColumnEFIssue"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201808032317064_FixSymbolCreatedColumnEFIssue.cs b/src/NuGetGallery/Migrations/201808032317064_FixSymbolCreatedColumnEFIssue.cs new file mode 100644 index 0000000000..6b77636486 --- /dev/null +++ b/src/NuGetGallery/Migrations/201808032317064_FixSymbolCreatedColumnEFIssue.cs @@ -0,0 +1,18 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class FixSymbolCreatedColumnEFIssue : DbMigration + { + public override void Up() + { + AlterColumn("dbo.SymbolPackages", "Created", c => c.DateTime(nullable: false)); + } + + public override void Down() + { + AlterColumn("dbo.SymbolPackages", "Created", c => c.DateTime(nullable: false)); + } + } +} diff --git a/src/NuGetGallery/Migrations/201808032317064_FixSymbolCreatedColumnEFIssue.resx b/src/NuGetGallery/Migrations/201808032317064_FixSymbolCreatedColumnEFIssue.resx new file mode 100644 index 0000000000..1e22262f03 --- /dev/null +++ b/src/NuGetGallery/Migrations/201808032317064_FixSymbolCreatedColumnEFIssue.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + H4sIAAAAAAAEAO19bW8cuXLu9wD5D4P5lAQby/JuTk4MK4FXtvYIsWzD0i7uN6E9Q0md7eme091jSyfIL7sf7k/KX7j93nypIovd7DftwIChaZJFsviwSBaLVf/7f//fm/943AWrbyxO/Cg8W5++eLlesXATbf3w/mx9SO/++c/r//j3v/+7N++3u8fVb3W+H/N8WckwOVs/pOn+9clJsnlgOy95sfM3cZREd+mLTbQ78bbRyauXL//t5PT0hGUk1hmt1erNl0OY+jtW/Mh+nkfhhu3TgxdcRVsWJNX3LOW6oLr66O1Ysvc27Gz98fALS3/xgoDFT+vV28D3sjZcs+BuvfLCMEq9NGvh618Tdp3GUXh/vc8+eMHN055l+e68IGFVy1+32amdePkq78RJW7AmtTkkabSzJHj6Y8WVE7l4J96uG65lfHuf8Td9yntd8O5sfc7i1L/zN16a9V+u7/V5EOd5Re6+4Mr8sBJTojj7VMMhQ03+74fV+SFIDzE7C9khjb3gh9Xnw9fA3/wne7qJfmfhWXgIAr6ZWUOzNOFD9ulzHO2zmp++sLuq8Vmm9epELHgil2zK8YXKXl2G6Y+v1lkXgsD7GrAGBxwHrtOsS7+wkMVZb7efvTRlcTaMl1tWcFKpXqrs+sE7vXk47L7uYz9M63ozCGbzaL268h4/sPA+fThb/5RNnAv/kW3rD1VTfg39bNZlZdL4wICm6qsnVf3qX/40RN3Xh6//xTa6irM/SRWbWBzF6ViVXSbJgcXj9Gmkut4/7v24Ei9lVe8yqN9kYlgp+dH75t8XWSUamViNOamQrFdfWFBkTB78fSmHebFxqxa4iKPdlygQRZKS7/Y6OsSbrGU3ESHzjRffs1TsxpuTVgZqJaNEjSwdpXJHCamHH8cqQr16Wjnr7YmgqBZGfyhA1xglAbpGv828BJueJ2jbDGZQGgvnglppNe2s5tpxghnk+87zg7fbbcySxMEuwDADszNCeOfHO7Ydt94MCKGXL1loRX/6aYhdTtnLIIi+s21d+c9RNjm80JrWZfKOBSztT+h9mGe+yiDvX3ibDD5vD+lDjpaNsM53Jf8xyub602dv87t3zz4fkgcHLc7ZeF5Cp2hjMSs1o5lPmt6o+ewlyfco3n5hCUsnqrHdfr0r1hnDFkxeomKWC4Vf041tyQ9ekl5kTM/6E937YQcKXOnz6NAeMXovuzErRFtWCl52m/TbcrHgVloxSV1cpXTb9fQqk2UZ5uF2FRTbHNJaWicoO1gxFdqy6lr0Kb73Qv9vRdKVf18i6Qv764ElKd5KfSmp5brMcG+0JVz2UDMQhmIWfUSGTF+kTy/tOkfqE70rTnpAbLq+zYTG2rcyF7zxN7ZtNIdwU5Vst5++ZxswocFYHkXqoBltxU9OTsPaKlliafEVZmWZZMvCa7Y5xNnG83OUbaB9TYOEjE+KsEaygIcMKF+Xw5BRSdHxYASy160+ol276IrapsjxkDSE5kDWtmZlRz+A/OYFB12tA2l3b1johenldvCT3TuWbGJ/z59bBqurRcvAFVXbdnXH3QHi59FufyiIEfS8LLHd5bfnkxt/83vCzZAcz+YzRjazgH6S15pN9hOW1EXSLS8VWyEtpylripLBmWbN4nSi6LKR00un5aLoIXmlKHIfFwnjrK3Gp/dSUWwAVSrzuFGr1FlvNwahS63NgSagvwSQJxsqIsjnnXwINe2q0uUmFZ+R1pRpvaZ9oUirtAnk2c8XOgoB/eT4Odrq9gduZmAOGnhLapAQrFXDdVXFjiVlbiLH13U108A5yUP8ts3Zzk4wgzJP4Vy2oqPseh8lIngRp6gYO0kQvW6QKFF0RKaVMB/Zd75xnQQORKPPbuDtdueHTo6gNlcorpZvRcNWjDJ8m9F5cjcsGkfjC84vmpKY2iMJRGMo66171U+SXLHd12xaZ92xFR9KyWllhkFgYHKm7EUnCeNUvIhN6UPpMinmYd8txrOVUg2nh7i6Mc7ezqLIKIf4DLdlD6G2a7IprdfldbZ/6bRfcShrVj97CaugXSNgTRwUmc3DDIu8waUMIf22vJDi1Kab2kxqbM9Llnbh6bBWLW+RmssKNfHyZECwm8tso/geR26bZxlBUvcU0cqFOHmyKSWP6qp+14Onr/48xPbqMrl+8GJhhPvaRn6O2Z3/6G5u1wYdrq0/5EXKaCZCbXBlcPmF3ftJGmvsbNQa4aK6zkAlCF0Di/VakQGKZGkBlD3KC9MV+PjC4l30PQwib9vFilQRE9lY+ne+C2PsD9Hm9w50uggcAKiAyMFzKTNTk7Wj2KE3vC2gb3qdj9T4JrO9wd9fD362El3791bsV8rpOyNlJ/VJLjOaLaOj5cBs56hdRfosB7ZLwFHsm94lKAPl4C5i/xT79w/DXyVOYU5Fs0tz079sUjMvYR+jtLXfGqwyh6vx+8ecp15QoevXOBi89X/xkoe3wX0U++nDTreVob23N1c2vunl5SYKx2BltgPKrZZ7GxTUdK7TvJAzamyXye9XbhvniGhh97jfji6T8nrfb32wWkNJf8PCZJwZ6m9ZVd0Xto/i3vj64IX3h2JPgM9EF7O92Azwjx1HGdNKdl74GTr9vzFOIOf2t5asyn7mtj1jDHM+tomf9f1p3NoMRvinLx0J/i/M216xbBiKFU7BsPn2MTfGrubB203u5sgL+6vJrg+7nRcPb6B2490PvxO58dNAO6ud2OJXp4DRH258jOKdF2RTeuusBSbjwHxx+8C+McB+mbQ8FAe7wYddWBzGEB4fsjOPC4XR55jF5U69L62LIF8mQrbNH+5H8fA8byp8x/YszA6N5cO+sWqtFrm8uuFrvfLD88DPzsXmefeTi3nXxZDXkSOIiq/Z5jY9JFwbeK1XmWh/umcavz1kq74a3rhS7rbJoyjg6iRM2dak2yrWTI5/avpCPrV5vCsfrIl8HttminNV004xo9pQPh1tqZDJtqmCTNc2Vsh526j7lEaD+RSVpDaz7XV7Ve4v2XIRxQael5meNB2QcmBNl7N1bLR4fzaUJh/pg17tb9mTapHQwF3MqMKdT0fhLmSyfiJ/2OdIY9uLOPv9PYp/17a4yaWBi5IHY7aa0ZbR10+7r1HQDr6G1XJWldliDpTdUjZbhqOW4TV9+bkH/x1tVP9XmBWlciGyvbQoSx2vLkh7nP5W0p7WfVjHvadJmvXd+iBCQNka9UFvs/I/2SK4LXlE8TgodmK/QVKYXO/ZRlOXG+VMCe9mSXNfX6/5abWvxmYquPnuM10l5brdjBUKHyftOJNW58XPVgGcG+tziuRuDaLo3ga63TyPdjvmxF+55dnU4lTaljAcS+uM2A4Tyd3RRGu4YzWp+fK5xIEM6yi9jnKr11bXlZmnxnDNUg1kP+FIiiBlevaBbKWosYVsVewI2XGW2i7K+HwhTlJvt++9Oo9zoznWpdF4Bn5jGtuNZV41osnNMzb7GNEw8mi0MaJN5IysPJ1aYNmZBrozSHN4lLC/2kKOD9gNmCulf00f0f3zyaYmOrsJKM3T7LaIecpxf7jkWwBrk7Mpbhusrk0xbSZ4t9pnwnC6X7tZ0xQ8Tp0F69i7XY0j0hy/Q++EUOHOmu5qly91xOaoGvYht1wGU0cnu8c/wOumIWwVv0TfpSX3Zz8sTlx5hOFDHOc31FdFm4sOCf1EdKEun3z0WratTXCwpRux1OkkGnPKdKcuWeajIJyVdUp+3ukYaQb0cyQGoekEKTUMjFWYSLHoEW5d1ORuz1LDxZ12GTHEcTiWISaq6wBMUDAjXaCmTtP5/FAg84Ll6yE1nlFb5jiBZ7VeXHlhtmlAlgxu2G7bjFxcFCBdDY4CZXLqy4avAdpOQenaZmqtmW0niu0xUyx2nC6G82E7bL2XPeDRQ38P8Yd8zPMAvkHwVDW294PFcBMctv3pDHT7hr/O4lcOV/NYiRSrm+yu396IcxXymgS0X5Mbk0q6Ir1kVCVXrlmaFqNPlFFisaOMMswz9piCjjMczTlbzXzhT87Wsz9Q9jju468nefAJOARZNz/6Gf7c0ONDA5xnQB3FqmOQQAB4fDBgDty2BRT9HZgPu+KAMztwNUvuiWH1IhYh9Y+6jultUQUAk7uplNN3UcpO6p5cxoX9QelYwP4dV17qKKhJThs+hfaCBKb085MD+ewlowRxlLVw/n3o5TgYoWqNg4SKjbpZXWa65fIqM1nOgs1eJd8QvnarSjQXPlIOQ2vdaCnebja5v0hL8SKUOoqXkcVLxf3eMsadtHIpNSyFRMUMcOYJML2VC7TzT5dPmYXazPauYHSSDqxKlHRIFlqrEUmHC4/DTt2Z1Hf7l8lF4N0nzbDa7VRKKi5DOP0ablkcPGUw5JEtjksZFaUWhd88PyhdnRa3QWfrl8pACgUaZ09V9lN99iyXv/VKRUtVQn3CIpS4yBrEtnW5fDtQlftRHbByaPiPb5Mk2vhFwXqayoGXxbrfh9uVKQpza9pRBSq/ygbD32fszwBztv4npUMaoo3uriXKB4UWKZ+u5bXgU1gOwKoMeJ2dQb1k421VEZJxZyt+yZYPFpf1ZAfX/Azih6m61vjhxt97gaH9UjnqKpU3rKlCTqkfl6eGAaHULYU/V1vRVCaxzcSlNyccxCjIK09cenxIx6zeeBOPYBy98lZVJPfyxQt1EvcAi1D5eDgReEipttUBTYIODtdApL12RKV8EEpwAaKDikyYCJfpZRLS8DGghgwGperGSmUStMEhxTFoGOKLtwDhM9pgTx+afDSBpW3GGHjSMprSgLrMdMgSg8BjAw7HfB9T1GBtAUBnh+pOyAP5MQbiwM5TKr6JZoAzbXhx7XiTIocb8YhLNIuA5m0l+mjpQ0k9Mj9GwyOFb5TGSGHqZwxWg6TUFp2D+NQ3sCfUXcpYEidnAXQraVzEDp5WIOsCemPYIUX3hkFjI4xJsd3batqo2MMCktD7MZBI4A7p/DwLSQtGjbbARQfcDSJV9dGtIayOB9Kp0blAWKJRmbHxN4dobjEAhCSnH72NIZ4NuwqJtgVPIAsYA1fwIhBfQGseOmc0lY3Fm8aLsqGNajQJhQsjyS25QQCnqiy1X/FBpBbClzEEFsIBStX8i/FJxFTddj44iGmkwUghRvgRJh4YX4RXc3ONHOxwrGnLmGACmEypXg50MymoBI/TprGHw7pMLdVgp9hKq3in84OKN4hLY8IS4seSBB3iqdUAA5OfZAUP0ssHa0lo8hOLVeh8PwJ7YrZqveLmZmBuyd5yRpYhfaa3tiMjTnPtAC5pusve/wxAQl0BKhBq/CdbgxWNpLUcmCJdGBGgyEAtEZrae3gg76CgnOYWXtOICTC1LLsO6ZVs6Q/RtKvlM89nry20CheL+ZeBd9kQf8bcZUOcWJJsU/0gGsZe4xRRGX/O96O1nNPEJTSiX5V3Haap5P/MNCWweIRTT1XMSRtnuCq6mxx0usJcGnPCwvxY0pQl7UNMG5AuOr9JtxxT7TWWucmAw+0aBlgfTtfu8mZYqWaIEtwB7X2gqeXbiFDVcsVCwikeIWaD5CpUxjZ/z2d7FymVHelSUq518NtJ9c4YaJbN3TZUfISbbrBafIZbjJmlbSDk7BDrhtHzoTjqsttROr9MrhNn/zDE0IExpKZhsOa/1itmivYGq/Z2qhZm1XOw4zP2eCykaTlDacRH9r1k2owAZ4G02ZlCT2Oyh/NlEiBaIXAW8Cs8futhJzoFt9tpYdQArOQJbrcaxfrD2YUY+innnsMMU9qE7Eg0BkIuZxrGotEmG8aP+e8ueLMmMi51hcDn0DgMBkKptoWTgZXCtzEwS+EOpR2zMG4D/YOjuNA6C+cQy3sJtnjBr3MzPpgygOAA2NBgTVENdzoomS1cCU+oiew2ucldG2WKkwd2cSpD0EM3ZUbqlN2aCT/UEqV1JN59svUBL8imEeEKMoO0Ekke+KfUZcOeeQ3qZIObXkUWiu6zrbXXeke/s9crUnox4pWMdvCIWp+JnR9BPbHZSBDLj4To57SvsOzfVLB/TjsMqH+yx24bOKLuuwedBJgD8EVKd6QzU6EdGVFKc6CYB1OiXPHrasAY7uQVeIVV+nO2xjHqIHYp2MU6MCJesXGiNEH0yTwDdFKNm1Dn5URsggZtxlo6myRZ8EPrRRprL82ldNtqyQ87fd6S3FHPfvJSejHGDKYMm8U05j21TzKXMQ/iVmiCVxq3iLVZaZzoXQytmAxsM18u3hee4bMyaVaicVRefPVZkn9nj1BUsWwkK//vSeXoX4ZHTveapeoFR7JevW/80UP3WgrSRFLK5ZpKTrmEIpDE6BgLt45zwZ5xDowNhApnzxCNylW2oTjv2hOiIrr+NBDTO/hTiet94BkqU4yboBoACygyWT09IyHF2hKiB9hzGsiChqYqYVC3QiOtIUclUXvtQQnVvm9o5ATHGShN3ukEja7wrltDWXqQb0XcTJZKsHyQqWVB8+KVRrF8HYlSK18W0kg1L8k09LiXaiahJj4LA4Sb+JzKNBULcyZg9hXGRQQxL1iwwiMAmRqb1oD21gRcBPg7MRopDcfkeywDQTG6K0RQDhtLwwmvxtFARVTEUaVEvg3SCohyl2ogJ+zIIHLSplcix+2ZpFWaD7qy4rLxCzYamEXYPaOhWZq+tBsDZT+oIVPvvjkywkZFPveJfSXzoQq6iLEA0PACzRZ1uV06LuplOQrVzq53b+WQFkCPtVEvhDZjcS9oY6UlNSAL4DgLACMIARmEPuhDMnA9kXa4GrbogzAMwBwxVADAFE0sAaHlcDQBU4s1JIBOa/nYtfP6EwDCELrTe7WHJLf39owjObrnyBpOSsMzF4ebhZd2Oz70ASbNvfqIDNY5+wY4S/YNLnSd4h3cipcUP+AcQeiIPQTnqCyz5JU7Jhm444ItqINkgDU0Z8pCp4zulLmOQZoLDb+MDpQHWDkhcwecX5rceLfwQhDPYLWMhmsa8sPzrXGrjDML9rwMdUHxvayyhcQKxWWySqdptjNGCApfnBmoP2CwI5BH4K5MgXwA81t+QaftjC2ixg3nC+7SFuwM6NS2K2dAX7QqMbErrjiEOE7FWUXxtAp10+BrVe2vrNQ0c9LgXRWtYjB2NkoqIjfr/JY9rYE0HC/rGnBWOuSh7NwT557WDSjUK8wRqNofTkltZhbm+3MUNmGKCCgbuSeYEqIPd4bWQtTcFy8FcJkv5DOLaT57b5kvEMPhUvXBFWtU/304ewy+/qBe4d7+1J7x9yxmfuH+/QadYrJbPw2YdA4AQQQgLgC7Agrx3Merk6W+OGOSQQARJY9J5NDYMJKMgT2mEU9sdJDofay5PrXpwOMeNlpXXUROaj18GfuL+fhyzVfMq9cA8CT68yKpYsCSNtoTiMAgahqwIhzE2tHsqKOG/H4himmjizBFa6xzEiaBSL3gN2ikdX7BhropAr1QUdT4XbT3jpX2o6uZATdKFFZZ8cgVc0ZQL3NufTA2AE5/1GaLbn/suy06+uGlWdk2Nx1VrT2RPut9rqjNR92t2HMC9YsikXKs4dN6nIEMFMgeakQTA4qPGqIek054JBZCLk8g1hldo4g90zlH4VklmI3pWKVzhzKEuYvZEQrOJV0pYxc1hTUcJJ2QLFymDL9lAn1vGJBHODZpvXT0RZ7ueGQYiO6nJNgJBH48IjiNgM4rercRKh4k60fzgUjvKGK4g7rREQGRlXazmUhhMAZPO7mhFsnP3ols176WN7IBey/vmt3YC/nhYK08gcP5qX8tB/UNfS8H3UlWtsVmjqEv5AbnEkE/p30erOkNQSdnz6KR9HDal8AAq+gvh4W+kd4Ocz2UrdY1bCO9Fh4AXtg7VCrTtJPQ9GjVIavcTML81XReuHlG2aS9ObnePLCdV314c5Jl2bB9evCCq2jLgqROuPL2+/x1Rluy+rK6zhV82f7qn6/Xq8ddECZn64c03b8+OUkK0smLnb+JoyS6S19sot2Jt41OXr18+W8np6cnu5LGyUaYz/Kjz6amNIqzmSWlZlVnLb3w4yR956XeVy9/5nS+3SnZig/co1GRew1769qAd6Hq4NXPNepC+d+Vo6fDLyytHq680Nj0tJy8yDq3Y2Fa9JPBB1a1dFb+euMFXlw/1uUfC59HwWEXal4P4+WvH7zTm4fD7us+9nNpw5OS0+hUMYrdqF0fvv4X28iNqz/a9DWKU5iYkEKneJkkh3wPxdOqv1m2CyIlJNDpvX/c+/WmmifHf1epvTmRACpPgxNlHkiCSZ5bpJlH0AARZ5/82tp+BhopDDULZb+7PCmTT16cauOImieHeqeeFAF2w36Z5H9/uvsHZfz/seOojzfUxaOXt9ttzJJEmp5CisUwh5sovPPjHdvixNFMdnAKs79UPJVfbXkQBNH3/CGqyoM6xUYQVzspWRY3ny1aF3pfA1Z4B7nwNhlYchvh/HHbBpKppsz0ej9G2Tx/qs4Vnw/Jg9wbMIMl189LFBSNu4l+Z3J3kDz0Wj57SfI9irf55XAK1ACl96HeLmrvCotrfV1ybgsRHbNcsfhrupHEM/edTu2Dl6QXGavZ9kN074cKVSidTp0reV4eInnaaqr7pcBm3ebvLTuIb+1dK84jqhyfaFHkH952P4+gL78pxxFN4aEWR9sdC3r+KPxMCCcPwPOEjsJvXnCQSFSfLFrBQi9MLyUp3n6lU3rHkk3s79XlR0iwWC6LkU0lRrdfrcUiKBOtzyxM3hHVH7ucfW78ze8QPT7RTmBnQNyqYrr8OhvBUfkq6CwzSidP9uICKTfYiamRTuqBSUyi02zdt/LkcKeuOCVXGotqJ1x6SxSpSUmzwZ/4qL8zDAUvYfZo1BcfCpQ/R1uJQPnFYudW+YhQWiIkWACRKUo9No3y7SYC+8V9ng2Ebf0VdNjnmpxMWO57zeTQQyf7zhNSxgdKt5Bf2112eIHGXUyxkPv682uvo2vFPPUcKSTMBqaAkWtnbKpeDe0BSaCBrrw6CPbAnxh8VcK1Ji4mTvEyKZArq5mqj0cc98KxEwD3Qu70kEXwOgpYJwIA8OykMw5U96f2cCDQGGoH6ULtcJlcP3ix4AFWHH8l2Yb255jd+Y8yyfrrbDBFfDNFRBXB6JCAKxKVoZAlq6EsFVDR9zCIvC2gTpaSbLD0G4v9O1+9tGm/21D7EG1+V2nVX+eGzP5o7I7A8VAHQF4hieWx2Uztn2L//iGVN1HN5/EVmq5Vtl9YwLyEfYxSWUsqpkw7p98/pvl1cP24/dc4kDWwajqd+l+85OFtcB/FfvqwEwlLSXY0VVJWkmcThUpHm49WEiw3gEkVCVZ9tad0neaX0zC9Oq0DVbbLBPQrhGyd2LW1eupiFkv1/X6rzmwhwY7e+60Pkqu/W1ArXfwoIOK/WyDa38r+5QV4q8k2/Q7vD4W7ErHX9VeLleHwNfBVGwfus/Uqc+Fn2PD/phgBSIkWdOMoV3Mq48J/t5Hg+yjxcwc8CkEpqQtN9cpTTrOSiV+Yt71il2EprhUBWSbbKiXyy7wKem83uf2xF8qnE002G531bufF0jaj+Wihs/bupbW2/GJBwU9lAVx9sjgTsjhRdhHNRxv7onjnBdkU2IIUgWSbe4ZcJH9g35h6HyenWcvF4qQKSsYqxZpiKfgwecul2lBO1NWg+mZ5vo7LDR1wxm5SLG6UAi9NWci2jadL4VpJSe1AWfSHCJLXuUwk1SG64QLr0Hnq0tVx5YfngZ8dl8BpoaaOb84C2jZ2sGysOJTtotJDgp3DuNSxDJinPYnjbmntzuNVBKLOp3Ks/MBncwwHtlctnmwZXH6Z22gb/MvaDTkXIKrzsOtoLGPo+6kWK7l6vWcbcItTJths2vKHdFxkKHH/JiXODZ6Sf9i+ABUPep0xaiCzDJi6teC+zu9SleNL+9XuDAdsRjvtQs+j3U4x+Wk+zhTszmDeG+DjQXspy2XrX7jvGNWRBDuPEUpgGeLHmW27n515U2+3l9UKzedx1RPgubLDcdLlJY5ze3lHKn63it4lqCddX1r9MdWI/S+phrg6G0rt7vqypstVw7SrLuYJ327FLdT+nZdbuPQy1lr67sqV0n1avHA+/PuCpj0bd0aOhsRQ8Fnood8Yq4D6aEsIXm0/cIbyy5j0rix3XN5Mw2tjl0VxnrYvyFVCpzuEL9F3UOry32czczHX1lQz5TxOvP08hYv9YXUUoJ/9zmOiOuHv6EzGRGSo8XKlYei/e7o+fEUO4GLKMIbxU7ls4H0Zd0YhR6WL0wZd6T+snJCdQvcdnO4bLROBoYaIw4V6Py+lzc26++0hjfKXcFla8FQ1VtE2Ajks1HvhJjhslbcDzVcbGytF04WquCaaDBWWs++5n+Qek0Ek1GEymAgMJq/YY6oxkAWSZzN4oJfsvkd+nlr3U7+eysBHyIFFUO0nH3we38E/iOSnXCELpdvcpLSPh3OnufKFipxq3e6lvCyWHH/3nSclnT6WPnD5oeZGZSH4SbkDaz5b0/r5SWmUmGKDJS9RjvrVN4uThn8feukhlu0w2s+zQaPkJLwzGgU6HdBoKL8gNFY9wSDJJ0+J9DliVHQib3iPf1usQtkmlvrovilgfFqPBV1UhAVcQ+2MX+UfDcMKXXBoc2Y2LerR2MrHf9fGoqocvH1ytABrLEABbAxo0BUhPIknIgKvpS8mqNtFS1RoGrxcXAjnpNvGns/GKpUrRbE+1Rvs4XyH6+wvP3JyDuGBNLMnQio6E4EECHZPvINuC9heNeMcRiPY95QXtw5RoLaxJ07t25YdE7d+Pqqry+TjIQjO1ndeID9K03R9kJVHiU5usQQpZYlr0WnHxQgJar6AVQkJk76k5Ykafp28oYWLG7e3BJc/pA0kVP1C9r5g092I3B6ToTfC+PDXmgvlOgt0Twzd5gMsVaNkjzgTTS3qOZA5kbFHDg62bL6O5TJrrl2JxxddaOauzDRd5FmOsi7Y85LWAjimo2nvoGQn+tLC11gkUmVPGVhRdbgXQIJfznPriXffjBslXqKcpVHCVV+a3028xCpWoRBEseBMHhJx1SyCQPDCMst6lTHhm7/NAxdePyUp273IM7y4/mtQeh1oM2RT0L9jSRnH5mz96uXpq/XqbeB7SRnhsgrL+HpzSPLb8DCM0ir+JSFO4+mPeZxGtt2dyMXtoz3mVJJkK4QN4bTbtXRhSJy1N9lYyoiokfKF3a1QVL05kUu+gbCZt+FsXYQgLKb0Lywb+dIoI8293fBxMHL05R6nGgSeaOnLARPLqr558ebBi9erK+/xAwvv04ez9U8vrYlrCMtj9voy3LLHs/V/F0Vfry7/zy0fYu+2JfVDNsV+Df2/HrJcN/GBrf5HaOarf/mTPRNq3/BlI8Oqlf+w8x7/kSeWxqr1lspQPi6jA4J1MEVXbXNIj4/OWJLLH7qk/o69yoUA2/hJMZv/VU+YvxjQTkBtsMMFT0LZ3QhXFWGeyOEob0Vy1f7lh9WnOBPJr1cvoQlk2eBmTzRoS0+tW2qFpGcDHzE6ojivdbKRMsXRIIyOq2lCMmro/umnjqypgzOWtL/6qTUdLipjdyLGwIvdSYNxFnu0FImpqBmcH1/ZjjkUVnHgCuRYiuKiZUuf98LRjxIUO7EfRTViIieA3MhQLNregiVpj3VNWGI7LKmlw0u30q96bkAX1bSWNtEA3a4BgqMDt6RbQLil2zzVkycrAszzaLc/FAVsBXIdVrCfVFCiCdZLxL0kF6hCq4wkaNMosngBovMtWLJIEf7s5ItQuJeUaW1z7VpQl8MqJ50+HR6KpVCCBop9Vjk8LN+C0VjG+HMwDkKMPztIcUV7oYq1m5suW12XqOQiA9rxoiloMbnJCKZH3aMh2hxfzwxwiIYdy1QKvQSjGNzPril82V6NMJ64KJJNJSu8dMDX6j4YMwTSowHLGIIMBiMaZ8wMw54YdAlAsSPWc6Ep26sRTaC17vL0OWB4LPA+D+R2hu1EmCUDwhBUb8H7P7Ni4PTVnzuMhBKmr4/GtQ7Q53xcjRZuCx5Zg1qmy7BKkbWoekRoSNsoeX2AUcfHGwoYzwYMmM2hnZiGqfTc6DYeUh0cu0bTwmk0lJ0aLnoUdUDQ2VSFgt45aJ/k9ksnqF52oe1c1904zHViyFGHxesl/IRQeA4o1ZHpXDXKAUHBaerQk5qPf9fzFo9ziexisqhR7/qwtI53p5sh1pOO8zA49Dgp3nmxuwsaNc7htBNJLribdkrReDN4+rKLsKzj4YGYorUQdVjdRx1b+ap2oY4tXFa7IFQ6mHd7d9g4iHR75wvExCNXQNO3i6HxwD2GhbQsDotORkkNheeEaNLT6keMgtedjhr3zsX9DRztziVlMcadA8pqeDsNwH+yBvgwNiA0A4jeJmZqQLzuZ4B+Vqli6T5sAZ5NdDrAtgTorbFVHUAB8ZavQOjO8963Ip5qE0oQGrbDhgW0Ow5dD728lSbSYsdURtdzuxlT/O33IW+LPk2ouiMAeygZSea5tFNJEyOv+3KK7kt7K6maYHkTgPbZwBVaaSyvTmxZCEaHWzALJ5/x023duVB2PUXNILoGl0dGt3c3rq9UXCrsHetzF6BydHwd9cfRENpePk19WeZWma69r7FdRPAbBaeLb6FVP668057TyVrxAc78iB++BaPA6SEWBVkPDdxAujdNKLgFj+bkcxo1qxlCopOuFNysVoszQnGizedD0pVU4uj7t/pLv8t6ukWt4j5uwRN0aN20KS7bglk34Ttro56nowOhTm+XuxtruwQaGoFtwQgbenLqoqItmG2yP0bLO1+hdK9pOlvTZThwWw/zkCZkW3caXTU2ZLTrwp4tGO1A2LTBj514zLEFM3K2s1UIdmbXGq5oryZAgdHsWqJScPbKuIym5kjVO/D7TE1IsgVPHS6YVM9zthjryQ5ifNmeaC/DoRkgRdtVt9Gm7MmRYaWJLXaEFQMjk3XCVkugF8BmgXI6MntsGAiBx2gAJUZNUAsirs/N6IZrtF31VBoztA5w/BC56xiTw0s4HOXZ7vpmO9LksGDUiY2H8lJz4xG1KBO6qcl2FlcFew0o33S7+tuSI07g0+MMfnYz2D5O0sCLMw0vC12rZwJMq7uarpdbeIAZAC1w8CHzKE9449K02RJTZbEhRksb+4g2aMbARQ6l9GyU8vOTyrwfq2SywzuBB276z4UHqk+iuZfcW9QV9/twu8onkuCtu2p6HoHnBf+58Ey/D/xNVvPZWg0i+SksD9Cr0u1qRtNLNt5WhU0eKQlrR+nWl29C9UWs/Z8UotmYsbhs6nk22Jk899UQ459jP9z4ey9Qey1lpaIh705DVU6p342lQucoNUn+gNU6G9ISa01sEGJIUbBT6JRR2BTykR+t8oM4WC9fvDhVxmuqIYei2U082K3if5JxbrF2Cyx3lmM9kFiwFk9LQ4ulJNJEHRwBMbzT68ZR86AyQnCzzdMSE5aNAdyXOFKh4F57EiQUcVOrJssbrHlIjiNyoAo5V+TT4cbC87g1lnBRoq2VJ6zPSBBf88YMnftIA8yO3ucCpnlKpkGAuDTJ1RuFomf+SfDH96HyWm0EnqiZwEY7sRFsqpN9niyQOghyNCqXgSBkCC6A1DoP0QVAZyLMIGB5hihZEDyUi5NSHQNHWueGVXVNz48ukDrKggdbrGhbBi3HjiBocN8/EAwJVjtIzcSbwElhWT4xneVua17gG21/1R1v0+6pgLtZouyDLt75YQbTR4EgZs9laN2AMDQaKQyESpJlG1I32RBmYpjOWBbOD4ijycM+yJuFRLytnTUZBhcazHHhVTk7BdpRpwwp08ZEE+TWFamRd9swKYw4B7y4UQCXR7h2479b3eNYgNPVFT/Wz2EhQbvlZ6IL5UkBIbj4nr1w4VzyAm3hU5+LkMGcECO1zkXQCDbttb01dRMPvr7lB1rKMCYCdcb6ppbK2YfEqMZ98LCAJb9mQJqhe74wPYAN23syII6g1YB2SXDF389MiVfYA8kMF/R+on65y7q9eJ7Lyl75p14OxGqH2kBDmqTnAivQefhCADW4NeHUeBhb22UDhnkouTgvvAs4jOb1Q60ovz8XkVL0ZknypHF163KJookWxcsuT5JLfC7YQLwl6wFyOzlCbgXfwHOWNKITY+GxjJiyeERp3DXr4TQ9msbauIyoPh97q7KcPYpw50uVIPO1ShgRUxNbHTwPK4MqeMo29zX1HK1iTinWCKfPE6EGhzHPBbLLs5CZFJRT28jQUDjt2qxapgKdeS7m0yaZTivwnI2ru8rxWdtag4imXUfOcPlfEqqn2hY4w/WstghqAJP5+p8Agq3ILZGTl70HMEWXQaqd2atyV+9/5/7ocjRYdHtr+ZF9LwvOCBrzfBM+1XvMCQBEqm4WsMlbbjwzTL065X8p9Zcfl40Y2HcmUtkM1p8SLYb9uDJa0Eg9I7SQB3BUtOQZJ0ZL/h/3DGCeq5LUSKUZmvcgS5Q2tk9GphU6XGvpaOrwsufZYmvsF0JdIDaLl0Kcq+DbKy/07gn3TnzwSAFo/PdxXEhKXp2R1gz4yBUNpDkQ0LR+rDGgmRxXTwa0Gd8WTQ+t8fyUdsDUxOujEIoV0p4vUGsuxZcFAPc8TTh0cXWRKmel8+Ylm9FmaHZr52Rwm2jltIHZLBbOqsF8ENkmUOksV06gwZCEFdOXvYJCXaLUK8SqnQ26nsdyOh8YTmx5Zo3KWa2uEECl6MhHKaiF3+ylIBQue0qslaPfho6dM76qGMagv5Qy5VlgCgrVjNQoRgueAY6o7whsh3J270rsx2oC6/9qUKYDhxB8XIqUPUtJI0ZL5xshpSxb0miCwiM1AmHS54Oo/ssWLlqOiNAjYsq1R/A+n49MS7AI61TcJ97eRG4Np3CsdHRwv/DgK7383ztHyfsismRWJs1KsLjWbkVbduHHSfrOS72vnhKPvCx1zVL1vnK9et8Eq4Ruma83D2znna23X6NsyMuYl8IdtooXsS7lllipT8kB1ancnVPqRSrDazCT5WPaqZzjEkHGNenmiqooi0od1XeIfJFkpiyG1lIqEJOhevgc5ur08XGU6vXZoeboYwuZmgcYASttAvJADVGy2dSurdZUn7ki4FmUUh+QB6pWyWauHdQdKvWDuaAWQG9sqG3A69XWRaZfe0/GaqnTNXXVPqSJNfLuVLFa+TyamgUvs8TqJc9vWAukbJpGCDltm2FsAKVqcqWNIyqs0iaDptIyjwXHS8dFWJVlqqa+0m0TsS7OGw5WIZdFU2uTi7D0iT5T1CVQTAeXQtFzjFE6Fra1qkAsPoMyMDcjJu1B5IdH4I5EzoTtT4R8FMQIl9HqfoVPBTcsbQZyXfi4yRk0NZJH7hcvCFicf8i1zkClcgaoUjEPeW6IGn9seoi5NDOEz2gh/MvzOS74y3St0M+zmGuUNAJKjVI6VKOQpcOmBdnUI/lIm5cyHkiXHQzSGDQncSdj1yCtD2DSWmy/PJblbNdKwBEDlXenZOZJTizsEUbbqdKKkfBnt4ltH4KA6wi+YNWpVksGgnAlh2HpqI1ErY6KieFsmJgPg2qFnFZFOmnf8md3Lht/6r7Fw8Dz1liKhiCrSvysKIo4T3L8sb8oWH2R1fxiV8jdLK/Z0R4Ct/CAEpFrXflhkg61HC1fpgOdkrO47Jj1OHfoIq9pua3jyUMdhTO67C6kNSpKigm9u1wotVvlktpVMcNiu6jXcyHdtonH3IsVFKVdQUmfcXhW4RCxCDn+TJili3QNcIkcGFt3C4T0Tll6BRqozrWgA6QOwRoqT8ZjBsIFB903RSoGOGEV3FjoEKpqLvoFpGqYoz0BwvSg6eqSZdVLJxuWQY+jesqc6RgDHHXMaDIXwruqOZIVHQXTNawzHOUxmsOzD0eWuZBLbM2CQU0kTZwdcLBNqCtQ0yksEC9z+NJ1irPuCjfgeJc1T5OxB+T84YT/bu4+hW09uizeNuF9xmMfuhxr9RqNp8CnumKAPvgezhCLoH1QR8ELO76vUgYz6whKSJS+nH1g5hplrEVAOQ0rNN1+HgxtJhaRkYh9r7vp2w/V3dkix9nCGaKNyOWSFdKtNF+8SXLdfUwxBWUbYK8yZpfF8EeadUvIN9ho89YAfNnyu6tOq9F68I4bIvu47LxinsAT4BKdjb1sWICPvi54jRMWgGYSpYZdTHHWecMkH2x2D7sPhUOFEE9h9PF1fX4dkSlINAsiiyixMKY48J9SDrSq6fowLLU7+1NiNQyjBBiNaXaxAkjaOB2BidSZJqjTCozFbBvVp47AeFN/rgxXjf7QxRXL6nKy44aKTXk52f3VntWN3iAXeePfSyn9ovV+qG4PdwNVdKEwQNKIbDWTa5DztlNNqfKjyy7iglLNpBFecmOhho7fRfUpFtJNJaPr0cTU3Epaf1sjnRtXyPCI7PbVqd5+ZKZoPI5CPKE6KBVZolqwlyzhv+vMtGBzRpmGo8srnW9MKksG2MxPwgRJDSbuoFBe6EqNt2+E3zLw/HJ34ga9DhrAQlA7OJo4o7CgogR7xsPPxARPem5OwdBLDx5XYvog7CDOImrR0bUvM2Ch7GyMyD6tj7Il40txM4IzRO+RxA0TxIdF4uV7meK44wRdr9bxk4tOjKjZ1borAlhAd2/Uc/zBh2VFUSllGBbAwCe54llMxzv6kjFZyHb0StOTbR2tbjVsy71m5UQaxypN2puT8pVR9SH7mUZxNh+voi0LkuLrm5Mv2VD5O1b+eseSQitfl8hohqxw2tUSrfNchndR7UxGalGdpU5uHEak3tZLvbf5Qc7bpFnyhiVJ8bT1Ny84sPy1wle2vQw/HdL9Ic26zHZfA0Fhl/ul0dX/5kRp85tP+4KnLrqQNdPPusA+hT8f/GDbtPvCC2R7FYxE7vDmF5Z9L8cy34aw+6eG0scoJBKq2Nf46blhu32QH9I/hdfeN9albRl8P7B7b/OUff/mb3MsY0TMAyGy/c0737uPvV1S0WjLZz8zDG93j//+/wF5/pVdPsMCAA== + + + dbo + + \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index efaee9b152..d03e6c5259 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -357,6 +357,14 @@ 201807302212501_AddShortCertificateNames.cs + + + 201808010014291_AddRepositoryType.cs + + + + 201808032317064_FixSymbolCreatedColumnEFIssue.cs + @@ -378,7 +386,13 @@ + + + + + + @@ -1536,6 +1550,12 @@ 201807302212501_AddShortCertificateNames.cs + + 201808010014291_AddRepositoryType.cs + + + 201808032317064_FixSymbolCreatedColumnEFIssue.cs + diff --git a/src/NuGetGallery/RequestModels/SubmitPackageRequest.cs b/src/NuGetGallery/RequestModels/SubmitPackageRequest.cs index 783241b92c..b2af5e3927 100644 --- a/src/NuGetGallery/RequestModels/SubmitPackageRequest.cs +++ b/src/NuGetGallery/RequestModels/SubmitPackageRequest.cs @@ -1,17 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.ComponentModel.DataAnnotations; -using System.Web; namespace NuGetGallery { public class SubmitPackageRequest { - [Required] - [Hint("Your package file will be uploaded and hosted on the gallery server.")] - public HttpPostedFile PackageFile { get; set; } - - public bool IsUploadInProgress { get; set; } + public bool IsUploadInProgress => InProgressUpload != null; public VerifyPackageRequest InProgressUpload { get; set; } } diff --git a/src/NuGetGallery/RequestModels/VerifyPackageRequest.cs b/src/NuGetGallery/RequestModels/VerifyPackageRequest.cs index da4f88760d..3a487f4009 100644 --- a/src/NuGetGallery/RequestModels/VerifyPackageRequest.cs +++ b/src/NuGetGallery/RequestModels/VerifyPackageRequest.cs @@ -10,7 +10,9 @@ namespace NuGetGallery { public class VerifyPackageRequest { - public VerifyPackageRequest() { } + public VerifyPackageRequest() + { + } public VerifyPackageRequest(PackageMetadata packageMetadata, IEnumerable possibleOwners, PackageRegistration existingPackageRegistration) { @@ -34,6 +36,8 @@ public VerifyPackageRequest(PackageMetadata packageMetadata, IEnumerable p IconUrl = packageMetadata.IconUrl.ToEncodedUrlStringOrNull(); LicenseUrl = packageMetadata.LicenseUrl.ToEncodedUrlStringOrNull(); ProjectUrl = packageMetadata.ProjectUrl.ToEncodedUrlStringOrNull(); + RepositoryUrl = packageMetadata.RepositoryUrl.ToEncodedUrlStringOrNull(); + RepositoryType = packageMetadata.RepositoryType; ReleaseNotes = packageMetadata.ReleaseNotes; RequiresLicenseAcceptance = packageMetadata.RequireLicenseAcceptance; Summary = packageMetadata.Summary; @@ -105,12 +109,16 @@ public VerifyPackageRequest(PackageMetadata packageMetadata, IEnumerable p public string LicenseUrl { get; set; } public string MinClientVersionDisplay { get; set; } public string ProjectUrl { get; set; } + public string RepositoryUrl { get; set; } + public string RepositoryType { get; set; } public string ReleaseNotes { get; set; } public bool RequiresLicenseAcceptance { get; set; } public string Summary { get; set; } public string Tags { get; set; } public string Title { get; set; } + public List Warnings { get; set; } = new List(); + private static IReadOnlyCollection ParseUserList(IEnumerable users) { return users.Select(u => u.Username).ToList(); diff --git a/src/NuGetGallery/RouteName.cs b/src/NuGetGallery/RouteName.cs index 0c64146b92..eff985ae86 100644 --- a/src/NuGetGallery/RouteName.cs +++ b/src/NuGetGallery/RouteName.cs @@ -45,6 +45,7 @@ public static class RouteName public const string PushPackageApi = "PushPackageApi"; public const string PublishPackageApi = "PublishPackageApi"; public const string DeletePackageApi = "DeletePackageApi"; + public const string PushSymbolPackageApi = "PushSymbolPackageApi"; public const string PasswordReset = "PasswordReset"; public const string PasswordSet = "PasswordSet"; public const string NewSubmission = "NewSubmission"; diff --git a/src/NuGetGallery/Scripts/gallery/certificates.js b/src/NuGetGallery/Scripts/gallery/certificates.js index 489e0060e4..564bf599e8 100644 --- a/src/NuGetGallery/Scripts/gallery/certificates.js +++ b/src/NuGetGallery/Scripts/gallery/certificates.js @@ -93,6 +93,15 @@ _model = { certificates: ko.observableArray(data), deleteCertificate: deleteCertificateAsync, + hasMissingInfo: function () { + var currentCertificates = this.certificates(); + for (var i = 0; i < currentCertificates.length; i++) { + if (!currentCertificates[i].HasInfo) { + return true; + } + } + return false; + }, hasCertificates: function () { return this.certificates().length > 0; } diff --git a/src/NuGetGallery/Services/ActionsRequiringPermissions.cs b/src/NuGetGallery/Services/ActionsRequiringPermissions.cs index 06ebacf262..901a1cc46d 100644 --- a/src/NuGetGallery/Services/ActionsRequiringPermissions.cs +++ b/src/NuGetGallery/Services/ActionsRequiringPermissions.cs @@ -44,6 +44,14 @@ public static class ActionsRequiringPermissions accountOnBehalfOfPermissionsRequirement: RequireOwnerOrOrganizationMember, packageRegistrationPermissionsRequirement: PermissionsRequirement.Owner); + /// + /// The action of uploading a symbol package for an existing package. + /// + public static ActionRequiringPackagePermissions UploadSymbolPackage = + new ActionRequiringPackagePermissions( + accountOnBehalfOfPermissionsRequirement: RequireOwnerOrOrganizationMember, + packageRegistrationPermissionsRequirement: PermissionsRequirement.Owner); + /// /// The action of verify a package verification key. /// diff --git a/src/NuGetGallery/Services/BackgroundMessageService.cs b/src/NuGetGallery/Services/BackgroundMessageService.cs new file mode 100644 index 0000000000..e6b2ed9e02 --- /dev/null +++ b/src/NuGetGallery/Services/BackgroundMessageService.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net.Mail; +using System.Threading.Tasks; +using AnglicanGeek.MarkdownMailer; +using Elmah; +using NuGetGallery.Configuration; + +namespace NuGetGallery.Services +{ + public class BackgroundMessageService : MessageService + { + public BackgroundMessageService(IMailSender mailSender, IAppConfiguration config, ITelemetryService telemetryService, ErrorLog errorLog) + :base(mailSender, config, telemetryService) + { + this.errorLog = errorLog; + } + + private ErrorLog errorLog; + + protected override Task SendMessageAsync(MailMessage mailMessage) + { + // Send email as background task, as we don't want to delay the HTTP response. + // Particularly when sending email fails and needs to be retried with a delay. + // MailMessage is IDisposable, so first clone the message, to ensure if the + // caller disposes it, the message is available until the async task is complete. + + var messageCopy = CloneMessage(mailMessage); + + Task.Run(async () => + { + try + { + await base.SendMessageAsync(messageCopy); + } + catch (Exception ex) + { + // Log but swallow the exception. + QuietLog.LogHandledException(ex, errorLog); + } + finally + { + messageCopy.Dispose(); + } + }); + + return Task.CompletedTask; + } + + private MailMessage CloneMessage(MailMessage mailMessage) + { + string from = mailMessage.From.ToString(); + string to = mailMessage.To.ToString(); + + MailMessage copy = new MailMessage(from, to, mailMessage.Subject, mailMessage.Body); + + copy.IsBodyHtml = mailMessage.IsBodyHtml; + copy.BodyTransferEncoding = mailMessage.BodyTransferEncoding; + copy.BodyEncoding = mailMessage.BodyEncoding; + copy.HeadersEncoding = mailMessage.HeadersEncoding; + foreach (System.Collections.Specialized.NameValueCollection header in mailMessage.Headers) + { + copy.Headers.Add(header); + } + copy.SubjectEncoding = mailMessage.SubjectEncoding; + copy.DeliveryNotificationOptions = mailMessage.DeliveryNotificationOptions; + foreach (var cc in mailMessage.CC) + { + copy.CC.Add(cc); + } + foreach(var attachment in mailMessage.Attachments) + { + copy.Attachments.Add(attachment); + } + foreach (var bcc in mailMessage.Bcc) + { + copy.Bcc.Add(bcc); + } + foreach (var replyTo in mailMessage.ReplyToList) + { + copy.ReplyToList.Add(replyTo); + } + copy.Sender = mailMessage.Sender; + copy.Priority = mailMessage.Priority; + foreach (var view in mailMessage.AlternateViews) + { + copy.AlternateViews.Add(view); + } + + return copy; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/IContentObjectService.cs b/src/NuGetGallery/Services/IContentObjectService.cs index 1a0eb8e2fe..038703ca92 100644 --- a/src/NuGetGallery/Services/IContentObjectService.cs +++ b/src/NuGetGallery/Services/IContentObjectService.cs @@ -10,6 +10,7 @@ public interface IContentObjectService { ILoginDiscontinuationConfiguration LoginDiscontinuationConfiguration { get; } ICertificatesConfiguration CertificatesConfiguration { get; } + ISymbolsConfiguration SymbolsConfiguration { get; } Task Refresh(); } diff --git a/src/NuGetGallery/Services/IMessageService.cs b/src/NuGetGallery/Services/IMessageService.cs index b971bb9c4f..acb684d4d5 100644 --- a/src/NuGetGallery/Services/IMessageService.cs +++ b/src/NuGetGallery/Services/IMessageService.cs @@ -3,42 +3,43 @@ using System.Collections.Generic; using System.Net.Mail; +using System.Threading.Tasks; using NuGetGallery.Services; namespace NuGetGallery { public interface IMessageService { - void SendContactOwnersMessage(MailAddress fromAddress, Package package, string packageUrl, string message, string emailSettingsUrl, bool copyFromAddress); - void ReportAbuse(ReportPackageRequest report); - void ReportMyPackage(ReportPackageRequest report); - void SendNewAccountEmail(User newUser, string confirmationUrl); - void SendEmailChangeConfirmationNotice(User user, string confirmationUrl); - void SendPasswordResetInstructions(User user, string resetPasswordUrl, bool forgotPassword); - void SendEmailChangeNoticeToPreviousEmailAddress(User user, string oldEmailAddress); - void SendPackageOwnerRequest(User fromUser, User toUser, PackageRegistration package, string packageUrl, string confirmationUrl, string rejectionUrl, string message, string policyMessage); - void SendPackageOwnerRequestInitiatedNotice(User requestingOwner, User receivingOwner, User newOwner, PackageRegistration package, string cancellationUrl); - void SendPackageOwnerRequestRejectionNotice(User requestingOwner, User newOwner, PackageRegistration package); - void SendPackageOwnerRequestCancellationNotice(User requestingOwner, User newOwner, PackageRegistration package); - void SendPackageOwnerAddedNotice(User toUser, User newOwner, PackageRegistration package, string packageUrl); - void SendPackageOwnerRemovedNotice(User fromUser, User toUser, PackageRegistration package); - void SendCredentialRemovedNotice(User user, CredentialViewModel removedCredentialViewModel); - void SendCredentialAddedNotice(User user, CredentialViewModel addedCredentialViewModel); - void SendContactSupportEmail(ContactSupportRequest request); - void SendPackageAddedNotice(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl); - void SendAccountDeleteNotice(User user); - void SendPackageDeletedNotice(Package package, string packageUrl, string packageSupportUrl); - void SendSigninAssistanceEmail(MailAddress emailAddress, IEnumerable credentials); - void SendOrganizationTransformRequest(User accountToTransform, User adminUser, string profileUrl, string confirmationUrl, string rejectionUrl); - void SendOrganizationTransformInitiatedNotice(User accountToTransform, User adminUser, string cancellationUrl); - void SendOrganizationTransformRequestAcceptedNotice(User accountToTransform, User adminUser); - void SendOrganizationTransformRequestRejectedNotice(User accountToTransform, User adminUser); - void SendOrganizationTransformRequestCancelledNotice(User accountToTransform, User adminUser); - void SendOrganizationMembershipRequest(Organization organization, User newUser, User adminUser, bool isAdmin, string profileUrl, string confirmationUrl, string rejectionUrl); - void SendOrganizationMembershipRequestInitiatedNotice(Organization organization, User requestingUser, User pendingUser, bool isAdmin, string cancellationUrl); - void SendOrganizationMembershipRequestRejectedNotice(Organization organization, User pendingUser); - void SendOrganizationMembershipRequestCancelledNotice(Organization organization, User pendingUser); - void SendOrganizationMemberUpdatedNotice(Organization organization, Membership membership); - void SendOrganizationMemberRemovedNotice(Organization organization, User removedUser); + Task SendContactOwnersMessageAsync(MailAddress fromAddress, Package package, string packageUrl, string message, string emailSettingsUrl, bool copyFromAddress); + Task ReportAbuseAsync(ReportPackageRequest report); + Task ReportMyPackageAsync(ReportPackageRequest report); + Task SendNewAccountEmailAsync(User newUser, string confirmationUrl); + Task SendEmailChangeConfirmationNoticeAsync(User user, string confirmationUrl); + Task SendPasswordResetInstructionsAsync(User user, string resetPasswordUrl, bool forgotPassword); + Task SendEmailChangeNoticeToPreviousEmailAddressAsync(User user, string oldEmailAddress); + Task SendPackageOwnerRequestAsync(User fromUser, User toUser, PackageRegistration package, string packageUrl, string confirmationUrl, string rejectionUrl, string message, string policyMessage); + Task SendPackageOwnerRequestInitiatedNoticeAsync(User requestingOwner, User receivingOwner, User newOwner, PackageRegistration package, string cancellationUrl); + Task SendPackageOwnerRequestRejectionNoticeAsync(User requestingOwner, User newOwner, PackageRegistration package); + Task SendPackageOwnerRequestCancellationNoticeAsync(User requestingOwner, User newOwner, PackageRegistration package); + Task SendPackageOwnerAddedNoticeAsync(User toUser, User newOwner, PackageRegistration package, string packageUrl); + Task SendPackageOwnerRemovedNoticeAsync(User fromUser, User toUser, PackageRegistration package); + Task SendCredentialRemovedNoticeAsync(User user, CredentialViewModel removedCredentialViewModel); + Task SendCredentialAddedNoticeAsync(User user, CredentialViewModel addedCredentialViewModel); + Task SendContactSupportEmailAsync(ContactSupportRequest request); + Task SendPackageAddedNoticeAsync(Package package, string packageUrl, string packageSupportUrl, string emailSettingsUrl); + Task SendAccountDeleteNoticeAsync(User user); + Task SendPackageDeletedNoticeAsync(Package package, string packageUrl, string packageSupportUrl); + Task SendSigninAssistanceEmailAsync(MailAddress emailAddress, IEnumerable credentials); + Task SendOrganizationTransformRequestAsync(User accountToTransform, User adminUser, string profileUrl, string confirmationUrl, string rejectionUrl); + Task SendOrganizationTransformInitiatedNoticeAsync(User accountToTransform, User adminUser, string cancellationUrl); + Task SendOrganizationTransformRequestAcceptedNoticeAsync(User accountToTransform, User adminUser); + Task SendOrganizationTransformRequestRejectedNoticeAsync(User accountToTransform, User adminUser); + Task SendOrganizationTransformRequestCancelledNoticeAsync(User accountToTransform, User adminUser); + Task SendOrganizationMembershipRequestAsync(Organization organization, User newUser, User adminUser, bool isAdmin, string profileUrl, string confirmationUrl, string rejectionUrl); + Task SendOrganizationMembershipRequestInitiatedNoticeAsync(Organization organization, User requestingUser, User pendingUser, bool isAdmin, string cancellationUrl); + Task SendOrganizationMembershipRequestRejectedNoticeAsync(Organization organization, User pendingUser); + Task SendOrganizationMembershipRequestCancelledNoticeAsync(Organization organization, User pendingUser); + Task SendOrganizationMemberUpdatedNoticeAsync(Organization organization, Membership membership); + Task SendOrganizationMemberRemovedNoticeAsync(Organization organization, User removedUser); } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/IPackageUploadService.cs b/src/NuGetGallery/Services/IPackageUploadService.cs index b0e964c345..c0c4784342 100644 --- a/src/NuGetGallery/Services/IPackageUploadService.cs +++ b/src/NuGetGallery/Services/IPackageUploadService.cs @@ -10,6 +10,16 @@ namespace NuGetGallery { public interface IPackageUploadService { + /// + /// Validate the provided package archive reader before + /// is + /// called. This is useful for finding errors or warnings that should be caught before the user verifies their + /// UI package upload. + /// + /// The package archive reader. + /// The package validation result. + Task ValidateBeforeGeneratePackageAsync(PackageArchiveReader nuGetPackage); + Task GeneratePackageAsync( string id, PackageArchiveReader nugetPackage, @@ -28,7 +38,7 @@ Task GeneratePackageAsync( /// The owner of the package. /// The current user. /// The package validation result. - Task ValidatePackageAsync( + Task ValidateAfterGeneratePackageAsync( Package package, PackageArchiveReader nuGetPackage, User owner, diff --git a/src/NuGetGallery/Services/ISymbolPackageFileService.cs b/src/NuGetGallery/Services/ISymbolPackageFileService.cs new file mode 100644 index 0000000000..cdae67b53f --- /dev/null +++ b/src/NuGetGallery/Services/ISymbolPackageFileService.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using System.Web.Mvc; + +namespace NuGetGallery +{ + public interface ISymbolPackageFileService : ICorePackageFileService + { + /// + /// Creates an ActionResult that allows a third-party client to download the snupkg for the package. + /// + Task CreateDownloadSymbolPackageActionResultAsync(Uri requestUrl, SymbolPackage package); + + /// + /// Creates an ActionResult that allows a third-party client to download the snupkg for the package. + /// + Task CreateDownloadSymbolPackageActionResultAsync(Uri requestUrl, string unsafeId, string unsafeVersion); + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/ISymbolPackageService.cs b/src/NuGetGallery/Services/ISymbolPackageService.cs index fc5542957f..310741c3aa 100644 --- a/src/NuGetGallery/Services/ISymbolPackageService.cs +++ b/src/NuGetGallery/Services/ISymbolPackageService.cs @@ -14,16 +14,17 @@ namespace NuGetGallery public interface ISymbolPackageService : ICoreSymbolPackageService { /// - /// Populate the related database tables to create the specified symbol package. + /// Populate the related database tables to create the specified symbol package. It is the caller's responsibility to commit + /// the changes. /// /// /// This method doesn't upload the package binary to the blob storage. The caller must do it after this call. /// - /// The package to be created. - /// The package stream's metadata. - /// The owner of the package - /// The user that pushed the package on behalf of + /// The nuget package for which symbol is to be created. + /// The symbol package stream's metadata. /// The created symbol package entity. - Task CreateSymbolPackageAsync(PackageArchiveReader symbolPackage, PackageStreamMetadata packageStreamMetadata, User owner, User currentUser); + SymbolPackage CreateSymbolPackage(Package nugetPackage, PackageStreamMetadata symbolPackageStreamMetadata); + + Task EnsureValidAsync(PackageArchiveReader packageArchiveReader); } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/ISymbolPackageUploadService.cs b/src/NuGetGallery/Services/ISymbolPackageUploadService.cs new file mode 100644 index 0000000000..5c6b7864e2 --- /dev/null +++ b/src/NuGetGallery/Services/ISymbolPackageUploadService.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Threading.Tasks; +using NuGetGallery.Packaging; + +namespace NuGetGallery +{ + /// + /// Business logic for uploading symbol to containers and creating db entities. + /// This will be the common code base for validating any symbols related things between + /// API and Packages controllers. + /// + public interface ISymbolPackageUploadService + { + /// + /// Create the symbols package entry in database, and upload the package to validation/uploads container for symbols. + /// This method commits the shared . This method can throw exceptions in exceptional + /// cases (such as database failures). This method does not dispose the provided stream but does read it. + /// + /// The package for which the symbols are to be uploaded. + /// The metadata object for the uploaded snupkg. + /// The seekable stream containing the symbol package content (.snupkg). + /// Awaitable task with + Task CreateAndUploadSymbolsPackage(Package package, PackageStreamMetadata packageStreamMetadata, Stream symbolPackageFile); + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/ITelemetryClient.cs b/src/NuGetGallery/Services/ITelemetryClient.cs index 99c7ff37f7..bdaf36a540 100644 --- a/src/NuGetGallery/Services/ITelemetryClient.cs +++ b/src/NuGetGallery/Services/ITelemetryClient.cs @@ -14,5 +14,15 @@ public interface ITelemetryClient void TrackMetric(string metricName, double value, IDictionary properties = null); void TrackException(Exception exception, IDictionary properties = null, IDictionary metrics = null); + + void TrackDependency(string dependencyTypeName, + string target, + string dependencyName, + string data, + DateTimeOffset startTime, + TimeSpan duration, + string resultCode, + bool success, + IDictionary properties); } } diff --git a/src/NuGetGallery/Services/ITelemetryService.cs b/src/NuGetGallery/Services/ITelemetryService.cs index 5516657395..7a5bdef870 100644 --- a/src/NuGetGallery/Services/ITelemetryService.cs +++ b/src/NuGetGallery/Services/ITelemetryService.cs @@ -136,5 +136,15 @@ public interface ITelemetryService /// /// The requesting the delete. void TrackRequestForAccountDeletion(User user); + + /// + /// A telemetry event emitted when an email is sent. + /// + /// URI to the SMTP server + /// The start time of when sending the email is attempted. + /// The duration of how long the send took. + /// Whether sending the email was successful. + /// The number of attempts the message has tried to be sent. + void TrackSendEmail(string smtpUri, DateTimeOffset startTime, TimeSpan duration, bool success, int attemptNumber); } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/MessageService.cs b/src/NuGetGallery/Services/MessageService.cs index 0ed979707e..e1fb64252a 100644 --- a/src/NuGetGallery/Services/MessageService.cs +++ b/src/NuGetGallery/Services/MessageService.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Net.Mail; using System.Text; +using System.Threading.Tasks; using System.Web; using AnglicanGeek.MarkdownMailer; using NuGetGallery.Configuration; @@ -16,22 +18,23 @@ namespace NuGetGallery { public class MessageService : CoreMessageService, IMessageService { - protected MessageService() - { - } - - public MessageService(IMailSender mailSender, IAppConfiguration config) + public MessageService(IMailSender mailSender, IAppConfiguration config, ITelemetryService telemetryService) : base(mailSender, config) { + this.telemetryService = telemetryService ?? throw new ArgumentNullException(nameof(telemetryService)); + smtpUri = config.SmtpUri?.Host; } + private readonly ITelemetryService telemetryService; + private readonly string smtpUri; + public IAppConfiguration Config { get { return (IAppConfiguration)CoreConfiguration; } set { CoreConfiguration = value; } } - public void ReportAbuse(ReportPackageRequest request) + public async Task ReportAbuseAsync(ReportPackageRequest request) { string subject = "[{GalleryOwnerName}] Support Request for '{Id}' version {Version} (Reason: {Reason})"; subject = request.FillIn(subject, Config); @@ -79,11 +82,11 @@ public void ReportAbuse(ReportPackageRequest request) // CCing helps to create a thread of email that can be augmented by the sending user mailMessage.CC.Add(request.FromAddress); } - SendMessage(mailMessage); + await SendMessageAsync(mailMessage); } } - public void ReportMyPackage(ReportPackageRequest request) + public async Task ReportMyPackageAsync(ReportPackageRequest request) { string subject = "[{GalleryOwnerName}] Owner Support Request for '{Id}' version {Version} (Reason: {Reason})"; subject = request.FillIn(subject, Config); @@ -126,11 +129,11 @@ public void ReportMyPackage(ReportPackageRequest request) // CCing helps to create a thread of email that can be augmented by the sending user mailMessage.CC.Add(request.FromAddress); } - SendMessage(mailMessage); + await SendMessageAsync(mailMessage); } } - public void SendContactOwnersMessage(MailAddress fromAddress, Package package, string packageUrl, string message, string emailSettingsUrl, bool copySender) + public async Task SendContactOwnersMessageAsync(MailAddress fromAddress, Package package, string packageUrl, string message, string emailSettingsUrl, bool copySender) { string subject = "[{0}] Message for owners of the package '{1}'"; string body = @"_User {0} <{1}> sends the following message to the owners of Package '[{2} {3}]({4})'._ @@ -168,12 +171,16 @@ [change your email notification settings]({7}). if (mailMessage.To.Any()) { - SendMessage(mailMessage, copySender); + await SendMessageAsync(mailMessage); + if (copySender) + { + await SendMessageToSenderAsync(mailMessage); + } } } } - public void SendNewAccountEmail(User newUser, string confirmationUrl) + public async Task SendNewAccountEmailAsync(User newUser, string confirmationUrl) { var isOrganization = newUser is Organization; @@ -194,11 +201,11 @@ We can't wait to see what packages you'll upload. mailMessage.From = Config.GalleryNoReplyAddress; mailMessage.To.Add(newUser.ToMailAddress()); - SendMessage(mailMessage); + await SendMessageAsync(mailMessage); } } - public void SendSigninAssistanceEmail(MailAddress emailAddress, IEnumerable credentials) + public async Task SendSigninAssistanceEmailAsync(MailAddress emailAddress, IEnumerable credentials) { string body = @"Hi there, @@ -235,12 +242,12 @@ public void SendSigninAssistanceEmail(MailAddress emailAddress, IEnumerable fn.ToShortNameOrNull()).ToArray(); if (!supportedFrameworks.AnySafe(sf => sf == null)) @@ -94,7 +94,7 @@ public async Task CreatePackageAsync(PackageArchiveReader nugetPackage, nugetPackage.GetNuspecReader(), strict: true); - ValidateNuGetPackageMetadata(packageMetadata); + PackageHelper.ValidateNuGetPackageMetadata(packageMetadata); packageRegistration = CreateOrGetPackageRegistration(owner, packageMetadata, isVerified); } @@ -513,6 +513,8 @@ public virtual Package EnrichPackageFromNuGetPackage( package.IconUrl = packageMetadata.IconUrl.ToEncodedUrlStringOrNull(); package.LicenseUrl = packageMetadata.LicenseUrl.ToEncodedUrlStringOrNull(); package.ProjectUrl = packageMetadata.ProjectUrl.ToEncodedUrlStringOrNull(); + package.RepositoryUrl = packageMetadata.RepositoryUrl.ToEncodedUrlStringOrNull(); + package.RepositoryType = packageMetadata.RepositoryType; package.MinClientVersion = packageMetadata.MinClientVersion.ToStringOrNull(); #pragma warning disable 618 // TODO: remove Package.Authors completely once production services definitely no longer need it @@ -565,93 +567,6 @@ public virtual IEnumerable GetSupportedFrameworks(PackageArchive return package.GetSupportedFrameworks(); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] - private static void ValidateNuGetPackageMetadata(PackageMetadata packageMetadata) - { - // TODO: Change this to use DataAnnotations - if (packageMetadata.Id.Length > CoreConstants.MaxPackageIdLength) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Id", CoreConstants.MaxPackageIdLength); - } - if (packageMetadata.Authors != null && packageMetadata.Authors.Flatten().Length > 4000) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Authors", "4000"); - } - if (packageMetadata.Copyright != null && packageMetadata.Copyright.Length > 4000) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Copyright", "4000"); - } - if (packageMetadata.Description == null) - { - throw new EntityException(Strings.NuGetPackagePropertyMissing, "Description"); - } - else if (packageMetadata.Description != null && packageMetadata.Description.Length > 4000) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Description", "4000"); - } - if (packageMetadata.IconUrl != null && packageMetadata.IconUrl.AbsoluteUri.Length > 4000) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "IconUrl", "4000"); - } - if (packageMetadata.LicenseUrl != null && packageMetadata.LicenseUrl.AbsoluteUri.Length > 4000) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "LicenseUrl", "4000"); - } - if (packageMetadata.ProjectUrl != null && packageMetadata.ProjectUrl.AbsoluteUri.Length > 4000) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "ProjectUrl", "4000"); - } - if (packageMetadata.Summary != null && packageMetadata.Summary.Length > 4000) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Summary", "4000"); - } - if (packageMetadata.Tags != null && packageMetadata.Tags.Length > 4000) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Tags", "4000"); - } - if (packageMetadata.Title != null && packageMetadata.Title.Length > 256) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Title", "256"); - } - - if (packageMetadata.Version != null && packageMetadata.Version.ToFullString().Length > 64) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Version", "64"); - } - - if (packageMetadata.Language != null && packageMetadata.Language.Length > 20) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Language", "20"); - } - - // Validate dependencies - if (packageMetadata.GetDependencyGroups() != null) - { - var packageDependencies = packageMetadata.GetDependencyGroups().ToList(); - - foreach (var dependency in packageDependencies.SelectMany(s => s.Packages)) - { - // NuGet.Core compatibility - dependency package id can not be > 128 characters - if (dependency.Id != null && dependency.Id.Length > CoreConstants.MaxPackageIdLength) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Dependency.Id", CoreConstants.MaxPackageIdLength); - } - - // NuGet.Core compatibility - dependency versionspec can not be > 256 characters - if (dependency.VersionRange != null && dependency.VersionRange.ToString().Length > 256) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Dependency.VersionSpec", "256"); - } - } - - // NuGet.Core compatibility - flattened dependencies should be < Int16.MaxValue - if (packageDependencies.Flatten().Length > Int16.MaxValue) - { - throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Dependencies", Int16.MaxValue); - } - } - } - private static void ValidateSupportedFrameworks(string[] supportedFrameworks) { // Frameworks within the portable profile are not allowed to have profiles themselves. diff --git a/src/NuGetGallery/Services/PackageUploadService.cs b/src/NuGetGallery/Services/PackageUploadService.cs index a87564565b..28318f5982 100644 --- a/src/NuGetGallery/Services/PackageUploadService.cs +++ b/src/NuGetGallery/Services/PackageUploadService.cs @@ -2,11 +2,13 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using NuGet.Packaging; +using NuGet.Versioning; using NuGetGallery.Configuration; using NuGetGallery.Extensions; using NuGetGallery.Packaging; @@ -38,7 +40,72 @@ public PackageUploadService( _config = config ?? throw new ArgumentNullException(nameof(config)); } - public async Task ValidatePackageAsync( + public async Task ValidateBeforeGeneratePackageAsync(PackageArchiveReader nuGetPackage) + { + var warnings = new List(); + + var result = await CheckForUnsignedPushAfterAuthorSignedAsync( + nuGetPackage, + warnings); + if (result != null) + { + return result; + } + + return PackageValidationResult.AcceptedWithWarnings(warnings); + } + + /// + /// If a package author pushes version X that is author signed then pushes version Y that is unsigned, where Y + /// is immediately after X when the version list is sorted used SemVer 2.0.0 rules, warn the package author. + /// If the user pushes another unsigned version after Y, no warning is produced. This means the warning will + /// not present on every subsequent push, which would be a bit too noisy. + /// + /// The package archive reader. + /// The working list of warnings. + /// The package validation result or null. + private async Task CheckForUnsignedPushAfterAuthorSignedAsync( + PackageArchiveReader nuGetPackage, + List warnings) + { + // If the package is signed, there's no problem. + if (await nuGetPackage.IsSignedAsync(CancellationToken.None)) + { + return null; + } + + var newIdentity = nuGetPackage.GetIdentity(); + var packageRegistration = _packageService.FindPackageRegistrationById(newIdentity.Id); + + // If the package registration does not exist yet, there's no problem. + if (packageRegistration == null) + { + return null; + } + + // Find the highest package version less than the new package that is Available. Deleted packages should + // be ignored and Validating or FailedValidation packages will not necessarily have certificate information. + var previousPackage = packageRegistration + .Packages + .Where(x => x.PackageStatusKey == PackageStatus.Available) + .Select(x => new { x.NormalizedVersion, x.CertificateKey }) + .ToList() // Materialize the lazy collection. + .Select(x => new { Version = NuGetVersion.Parse(x.NormalizedVersion), x.CertificateKey }) + .Where(x => x.Version < newIdentity.Version) + .OrderByDescending(x => x.Version) + .FirstOrDefault(); + + if (previousPackage != null && previousPackage.CertificateKey.HasValue) + { + warnings.Add(string.Format( + Strings.UploadPackage_SignedToUnsignedTransition, + previousPackage.Version.ToNormalizedString())); + } + + return null; + } + + public async Task ValidateAfterGeneratePackageAsync( Package package, PackageArchiveReader nuGetPackage, User owner, diff --git a/src/NuGetGallery/Services/PackageValidationResult.cs b/src/NuGetGallery/Services/PackageValidationResult.cs index 85a159ea03..b132a8dd05 100644 --- a/src/NuGetGallery/Services/PackageValidationResult.cs +++ b/src/NuGetGallery/Services/PackageValidationResult.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; namespace NuGetGallery { @@ -11,7 +12,14 @@ namespace NuGetGallery /// public class PackageValidationResult { + private static readonly IReadOnlyList EmptyList = new string[0]; + public PackageValidationResult(PackageValidationResultType type, string message) + : this(type, message, warnings: null) + { + } + + public PackageValidationResult(PackageValidationResultType type, string message, IReadOnlyList warnings) { if (type != PackageValidationResultType.Accepted && message == null) { @@ -20,14 +28,27 @@ public PackageValidationResult(PackageValidationResultType type, string message) Type = type; Message = message; + Warnings = warnings ?? EmptyList; } public PackageValidationResultType Type { get; } public string Message { get; } + public IReadOnlyList Warnings { get; } public static PackageValidationResult Accepted() { - return new PackageValidationResult(PackageValidationResultType.Accepted, message: null); + return new PackageValidationResult( + PackageValidationResultType.Accepted, + message: null, + warnings: null); + } + + public static PackageValidationResult AcceptedWithWarnings(IReadOnlyList warnings) + { + return new PackageValidationResult( + PackageValidationResultType.Accepted, + message: null, + warnings: warnings); } public static PackageValidationResult Invalid(string message) @@ -37,7 +58,10 @@ public static PackageValidationResult Invalid(string message) throw new ArgumentNullException(nameof(message)); } - return new PackageValidationResult(PackageValidationResultType.Invalid, message); + return new PackageValidationResult( + PackageValidationResultType.Invalid, + message, + warnings: null); } } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/SymbolPackageFileService.cs b/src/NuGetGallery/Services/SymbolPackageFileService.cs new file mode 100644 index 0000000000..d7064ee07f --- /dev/null +++ b/src/NuGetGallery/Services/SymbolPackageFileService.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using System.Web.Mvc; + +namespace NuGetGallery +{ + public class SymbolPackageFileService : CorePackageFileService, ISymbolPackageFileService + { + private readonly IFileStorageService _fileStorageService; + + public SymbolPackageFileService(IFileStorageService fileStorageService) + : base(fileStorageService, new SymbolPackageFileMetadataService()) + { + _fileStorageService = fileStorageService; + } + + public Task CreateDownloadSymbolPackageActionResultAsync(Uri requestUrl, SymbolPackage symbolPackage) + { + var fileName = BuildFileName(symbolPackage.Package, CoreConstants.PackageFileSavePathTemplate, CoreConstants.NuGetSymbolPackageFileExtension); + return _fileStorageService.CreateDownloadFileActionResultAsync(requestUrl, CoreConstants.SymbolPackagesFolderName, fileName); + } + + public Task CreateDownloadSymbolPackageActionResultAsync(Uri requestUrl, string id, string version) + { + var fileName = BuildFileName(id, version, CoreConstants.PackageFileSavePathTemplate, CoreConstants.NuGetSymbolPackageFileExtension); + return _fileStorageService.CreateDownloadFileActionResultAsync(requestUrl, CoreConstants.SymbolPackagesFolderName, fileName); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/SymbolPackageService.cs b/src/NuGetGallery/Services/SymbolPackageService.cs new file mode 100644 index 0000000000..54956c744e --- /dev/null +++ b/src/NuGetGallery/Services/SymbolPackageService.cs @@ -0,0 +1,198 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGetGallery.Packaging; +using ClientPackageType = NuGet.Packaging.Core.PackageType; + +namespace NuGetGallery +{ + public class SymbolPackageService : CoreSymbolPackageService, ISymbolPackageService + { + private const string PDBExtension = ".pdb"; + private static readonly HashSet AllowedExtensions = new HashSet() { + PDBExtension, + ".nuspec", + ".xml", + ".psmdcp", + ".rels", + ".p7s" + }; + private const string SymbolPackageTypeName = "SymbolsPackage"; + private static readonly ClientPackageType SymbolPackageType = new ClientPackageType(SymbolPackageTypeName, ClientPackageType.EmptyVersion); + + public SymbolPackageService( + IEntityRepository symbolPackageRepository, + IPackageService packageService) + : base(symbolPackageRepository, packageService) + { } + + /// + /// When no exceptions thrown, this method ensures the symbol package metadata is valid. + /// + /// + /// The instance providing the package metadata. + /// + /// + /// This exception will be thrown when a package metadata property violates a data validation constraint. + /// + public async Task EnsureValidAsync(PackageArchiveReader symbolPackageArchiveReader) + { + if (symbolPackageArchiveReader == null) + { + throw new ArgumentNullException(nameof(symbolPackageArchiveReader)); + } + + // Validate following checks: + // 1. Is a symbol package. + // 2. The nuspec shouldn't have the 'owners'/'authors' field. + // 3. All files have pdb extensions. + // 4. Other package manifest validations. + try + { + var packageMetadata = PackageMetadata.FromNuspecReader( + symbolPackageArchiveReader.GetNuspecReader(), + strict: true); + + if (!IsSymbolPackage(packageMetadata)) + { + throw new InvalidPackageException(Strings.SymbolsPackage_NotSymbolPackage); + } + + ValidateSymbolPackage(symbolPackageArchiveReader, packageMetadata); + + // This will throw if the package contains an entry which will extract outside of the target extraction directory + await symbolPackageArchiveReader.ValidatePackageEntriesAsync(CancellationToken.None); + } + catch (Exception ex) when (ex is EntityException || ex is PackagingException) + { + // Wrap the exception for consistency of this API. + throw new InvalidPackageException(ex.Message, ex); + } + } + + /// + /// This method will create the symbol package entity. The caller should validate the ownership of packages and + /// metadata for the symbols associated for this package. Its the caller's responsibility to commit as well. + /// + public SymbolPackage CreateSymbolPackage(Package nugetPackage, PackageStreamMetadata symbolPackageStreamMetadata) + { + if (nugetPackage == null) + { + throw new ArgumentNullException(nameof(nugetPackage)); + } + + if (symbolPackageStreamMetadata == null) + { + throw new ArgumentNullException(nameof(symbolPackageStreamMetadata)); + } + + try + { + var symbolPackage = new SymbolPackage() + { + Package = nugetPackage, + PackageKey = nugetPackage.Key, + Created = DateTime.UtcNow, + FileSize = symbolPackageStreamMetadata.Size, + HashAlgorithm = symbolPackageStreamMetadata.HashAlgorithm, + Hash = symbolPackageStreamMetadata.Hash + }; + + _symbolPackageRepository.InsertOnCommit(symbolPackage); + + return symbolPackage; + } + catch (Exception ex) when (ex is EntityException) + { + throw new InvalidPackageException(ex.Message, ex); + } + } + + private static void ValidateSymbolPackage(PackageArchiveReader symbolPackage, PackageMetadata metadata) + { + PackageHelper.ValidateNuGetPackageMetadata(metadata); + + // Validate nuspec manifest. + var errors = ManifestValidator.Validate(symbolPackage.GetNuspec(), out var nuspec).ToArray(); + if (errors.Length > 0) + { + var errorsString = string.Join("', '", errors.Select(error => error.ErrorMessage)); + throw new InvalidDataException(string.Format( + CultureInfo.CurrentCulture, + errors.Length > 1 ? Strings.UploadPackage_InvalidNuspecMultiple : Strings.UploadPackage_InvalidNuspec, + errorsString)); + } + + // Validate that the PII is not embedded in nuspec + var invalidItems = new List(); + if (metadata.Authors != null + && (metadata.Authors.Count > 1 + || !string.IsNullOrWhiteSpace(metadata.Authors.FirstOrDefault()))) + { + invalidItems.Add("Authors"); + } + + if (metadata.Owners != null && metadata.Owners.Any()) + { + invalidItems.Add("Owners"); + } + + if (invalidItems.Any()) + { + throw new InvalidDataException(string.Format(Strings.SymbolsPackage_InvalidDataInNuspec, string.Join(",", invalidItems.ToArray()))); + } + + if (!CheckForAllowedFiles(symbolPackage)) + { + throw new InvalidDataException(string.Format(Strings.SymbolsPackage_InvalidFiles, PDBExtension)); + } + } + + private static bool CheckForAllowedFiles(PackageArchiveReader symbolPackage) + { + foreach (var filePath in symbolPackage.GetFiles()) + { + var fi = new FileInfo(filePath); + if (!string.IsNullOrEmpty(fi.Name) + && !string.IsNullOrEmpty(fi.Extension) + && !AllowedExtensions.Contains(fi.Extension)) + { + return false; + } + } + + return true; + } + + private static bool IsSymbolPackage(PackageMetadata metadata) + { + var packageTypes = metadata.GetPackageTypes(); + return packageTypes.Any() + && packageTypes.Count() == 1 + && packageTypes.First() == SymbolPackageType; + } + + private static bool IsPortable(string pdbFile) + { + byte[] currentPDBStamp = new byte[4]; +            // Portable pdbs have the first four bytes "B", "S", "J", "B" +            byte[] portableStamp = new byte[4] { 66, 83, 74, 66 }; + + using (var peStream = File.OpenRead(pdbFile)) + { + peStream.Read(currentPDBStamp, 0, 4); + } + + return currentPDBStamp.SequenceEqual(portableStamp); + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/Services/SymbolPackageUploadService.cs b/src/NuGetGallery/Services/SymbolPackageUploadService.cs new file mode 100644 index 0000000000..f64bc3189c --- /dev/null +++ b/src/NuGetGallery/Services/SymbolPackageUploadService.cs @@ -0,0 +1,124 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using NuGetGallery.Packaging; + +namespace NuGetGallery +{ + public class SymbolPackageUploadService : ISymbolPackageUploadService + { + private readonly IEntitiesContext _entitiesContext; + private readonly IValidationService _validationService; + private readonly ISymbolPackageService _symbolPackageService; + private readonly ISymbolPackageFileService _symbolPackageFileService; + + public SymbolPackageUploadService( + ISymbolPackageService symbolPackageService, + ISymbolPackageFileService symbolPackageFileService, + IEntitiesContext entitiesContext, + IValidationService validationService) + { + _symbolPackageService = symbolPackageService ?? throw new ArgumentNullException(nameof(symbolPackageService)); + _symbolPackageFileService = symbolPackageFileService ?? throw new ArgumentNullException(nameof(symbolPackageFileService)); + _entitiesContext = entitiesContext ?? throw new ArgumentNullException(nameof(entitiesContext)); + _validationService = validationService ?? throw new ArgumentNullException(nameof(validationService)); + } + + /// + /// This method creates the symbol db entities and invokes the validations for the uploaded snupkg. + /// It will send the message for validation and upload the snupkg to the "validations"/"symbols-packages" container + /// based on the result. It will then update the references in the database for persistence with appropriate status. + /// + /// The package for which symbols package is to be uplloaded + /// The package stream metadata for the uploaded symbols package file. + /// The symbol package file stream. + /// The for the symbol package upload flow. + public async Task CreateAndUploadSymbolsPackage(Package package, PackageStreamMetadata packageStreamMetadata, Stream symbolPackageFile) + { + var symbolPackage = _symbolPackageService.CreateSymbolPackage(package, packageStreamMetadata); + + // TODO: Add Validations for symbols, for now set the status to Available. https://github.com/NuGet/NuGetGallery/issues/6235 + // Add validating type to be symbols when sending message to the orchestrator. + symbolPackage.StatusKey = PackageStatus.Available; + symbolPackage.Published = DateTime.UtcNow; + + if (symbolPackage.StatusKey != PackageStatus.Available + && symbolPackage.StatusKey != PackageStatus.Validating) + { + throw new InvalidOperationException( + $"The symbol package to commit must have either the {PackageStatus.Available} or {PackageStatus.Validating} package status."); + } + + try + { + if (symbolPackage.StatusKey == PackageStatus.Validating) + { + await _symbolPackageFileService.SaveValidationPackageFileAsync(symbolPackage.Package, symbolPackageFile); + } + else if (symbolPackage.StatusKey == PackageStatus.Available) + { + // Mark any other associated available symbol packages for deletion. + var availableSymbolPackages = package + .SymbolPackages + .Where(sp => sp.StatusKey == PackageStatus.Available + && sp != symbolPackage); + + var overwrite = false; + if (availableSymbolPackages.Any()) + { + // Mark the currently available packages for deletion, and replace the file in the container. + foreach (var availableSymbolPackage in availableSymbolPackages) + { + availableSymbolPackage.StatusKey = PackageStatus.Deleted; + } + + overwrite = true; + } + + // Caveat: This doesn't really affect our prod flow since the package is validating, however, when the async validation + // is disabled there is a chance that there could be concurrency issues when pushing multiple symbols simultaneously. + // This could result in an inconsistent data or multiple symbol entities marked as available. This could be sovled using etag + // for saving files, however since it doesn't really affect nuget.org which happen have async validations flow I will leave it as is. + await _symbolPackageFileService.SavePackageFileAsync(symbolPackage.Package, symbolPackageFile, overwrite); + } + + try + { + // commit all changes to database as an atomic transaction + await _entitiesContext.SaveChangesAsync(); + } + catch (Exception ex) + { + ex.Log(); + + // If saving to the DB fails for any reason we need to delete the package we just saved. + if (symbolPackage.StatusKey == PackageStatus.Validating) + { + await _symbolPackageFileService.DeleteValidationPackageFileAsync( + package.PackageRegistration.Id, + package.Version); + } + else if (symbolPackage.StatusKey == PackageStatus.Available) + { + await _symbolPackageFileService.DeletePackageFileAsync( + package.PackageRegistration.Id, + package.Version); + } + + throw ex; + } + } + catch (FileAlreadyExistsException ex) + { + ex.Log(); + return PackageCommitResult.Conflict; + } + + return PackageCommitResult.Success; + } + } +} diff --git a/src/NuGetGallery/Services/TelemetryClientWrapper.cs b/src/NuGetGallery/Services/TelemetryClientWrapper.cs index d5d7f1fdbe..e817cba55e 100644 --- a/src/NuGetGallery/Services/TelemetryClientWrapper.cs +++ b/src/NuGetGallery/Services/TelemetryClientWrapper.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; namespace NuGetGallery { @@ -52,5 +53,31 @@ public void TrackMetric(string metricName, double value, IDictionary properties) + { + try + { + var telemetry = new DependencyTelemetry(dependencyTypeName, target, dependencyName, data, startTime, duration, resultCode, success); + foreach (var property in properties) + { + telemetry.Properties.Add(property); + } + + UnderlyingClient.TrackDependency(telemetry); + } + catch + { + // logging failed, don't allow exception to escape + } + } } } \ No newline at end of file diff --git a/src/NuGetGallery/Services/TelemetryService.cs b/src/NuGetGallery/Services/TelemetryService.cs index 0ff51395dc..1bfb9efef7 100644 --- a/src/NuGetGallery/Services/TelemetryService.cs +++ b/src/NuGetGallery/Services/TelemetryService.cs @@ -594,6 +594,15 @@ public void TrackRequestForAccountDeletion(User user) }); } + public void TrackSendEmail(string smtpUri, DateTimeOffset startTime, TimeSpan duration, bool success, int attemptNumber) + { + var properties = new Dictionary + { + { "attempt", attemptNumber.ToString() } + }; + _telemetryClient.TrackDependency("SMTP", smtpUri, "SendMessage", null, startTime, duration, null, success, properties); + } + /// /// We use instead of /// diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs index a71c33d67a..778d9ef34f 100644 --- a/src/NuGetGallery/Strings.Designer.cs +++ b/src/NuGetGallery/Strings.Designer.cs @@ -1777,6 +1777,69 @@ public static string SupportRequestSentTransientMessage { } } + /// + /// Looks up a localized string similar to It looks like there is another copy of this symbols package pending validation(s). Please wait for the validation(s) to finish before trying to replace the symbols package.. + /// + public static string SymbolsPackage_ConflictValidating { + get { + return ResourceManager.GetString("SymbolsPackage_ConflictValidating", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to read the symbols package. Ensure it is a valid symbols package (.snupkg).. + /// + public static string SymbolsPackage_FailedToReadPackage { + get { + return ResourceManager.GetString("SymbolsPackage_FailedToReadPackage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid metadata items specified in nuspec. Please remove '{0}' from the nuspec.. + /// + public static string SymbolsPackage_InvalidDataInNuspec { + get { + return ResourceManager.GetString("SymbolsPackage_InvalidDataInNuspec", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid file found in the uploaded package. Symbols packages should only contain '{0}' files.. + /// + public static string SymbolsPackage_InvalidFiles { + get { + return ResourceManager.GetString("SymbolsPackage_InvalidFiles", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The uploaded package is not a valid symbols package. The required 'SymbolsPackage' package type is missing.. + /// + public static string SymbolsPackage_NotSymbolPackage { + get { + return ResourceManager.GetString("SymbolsPackage_NotSymbolPackage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A package with ID '{0}' and version '{1}' does not exist. Please upload the package before uploading its symbols.. + /// + public static string SymbolsPackage_PackageIdAndVersionNotFound { + get { + return ResourceManager.GetString("SymbolsPackage_PackageIdAndVersionNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You do not have the permissions to upload symbol packages.. + /// + public static string SymbolsPackage_UploadNotAllowed { + get { + return ResourceManager.GetString("SymbolsPackage_UploadNotAllowed", resourceCulture); + } + } + /// /// Looks up a localized string similar to The title of your package, '{0}', is similar to the ID of an existing package, which can cause confusion with our users. Please modify the title of your package and try uploading again.. /// @@ -2102,6 +2165,15 @@ public static string UploadPackage_PackageIsSignedButMissingCertificate_Required } } + /// + /// Looks up a localized string similar to The previous package version '{0}' is author signed but the uploaded package is unsigned. To avoid this warning, sign the package before uploading.. + /// + public static string UploadPackage_SignedToUnsignedTransition { + get { + return ResourceManager.GetString("UploadPackage_SignedToUnsignedTransition", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot upload file because an upload is already in progress.. /// diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx index 2ff33f3ee1..ec26971146 100644 --- a/src/NuGetGallery/Strings.resx +++ b/src/NuGetGallery/Strings.resx @@ -924,4 +924,29 @@ If you would like to update the linked Microsoft account you can do so from the The package was signed. The owner '{0}' must register the signing certificate to publish signed packages. {0} is the signer name. + + A package with ID '{0}' and version '{1}' does not exist. Please upload the package before uploading its symbols. + + + The uploaded package is not a valid symbols package. The required 'SymbolsPackage' package type is missing. + + + Failed to read the symbols package. Ensure it is a valid symbols package (.snupkg). + + + Invalid metadata items specified in nuspec. Please remove '{0}' from the nuspec. + + + Invalid file found in the uploaded package. Symbols packages should only contain '{0}' files. + + + You do not have the permissions to upload symbol packages. + + + It looks like there is another copy of this symbols package pending validation(s). Please wait for the validation(s) to finish before trying to replace the symbols package. + + + The previous package version '{0}' is author signed but the uploaded package is unsigned. To avoid this warning, sign the package before uploading. + {0} is the previous package's normalized version. + \ No newline at end of file diff --git a/src/NuGetGallery/Telemetry/QuietLog.cs b/src/NuGetGallery/Telemetry/QuietLog.cs index df32524584..a5d2d4e49a 100644 --- a/src/NuGetGallery/Telemetry/QuietLog.cs +++ b/src/NuGetGallery/Telemetry/QuietLog.cs @@ -38,6 +38,31 @@ public static void LogHandledException(Exception e) } } + public static void LogHandledException(Exception e, ErrorLog errorLog) + { + var aggregateExceptionId = Guid.NewGuid().ToString(); + + var aggregateException = e as AggregateException; + if (aggregateException != null) + { + LogHandledExceptionCore(aggregateException, aggregateExceptionId, errorLog); + + foreach (var innerException in aggregateException.InnerExceptions) + { + LogHandledExceptionCore(innerException, aggregateExceptionId, errorLog); + } + } + else + { + LogHandledExceptionCore(e, aggregateExceptionId, errorLog); + + if (e.InnerException != null) + { + LogHandledExceptionCore(e.InnerException, aggregateExceptionId, errorLog); + } + } + } + private static void LogHandledExceptionCore(Exception e, string aggregateExceptionId) { try @@ -65,6 +90,24 @@ private static void LogHandledExceptionCore(Exception e, string aggregateExcepti } } + private static void LogHandledExceptionCore(Exception e, string aggregateExceptionId, ErrorLog errorLog) + { + try + { + errorLog.Log(new Error(e)); + + // send exception to AppInsights + Telemetry.TrackException(e, new Dictionary + { + { "aggregateExceptionId", aggregateExceptionId } + }); + } + catch + { + // logging failed, don't allow exception to escape + } + } + internal static bool IsPIIRoute(RouteData route, out string operation) { if(route == null) diff --git a/src/NuGetGallery/ViewModels/ListCertificateItemViewModel.cs b/src/NuGetGallery/ViewModels/ListCertificateItemViewModel.cs index adb32ac690..0212d23559 100644 --- a/src/NuGetGallery/ViewModels/ListCertificateItemViewModel.cs +++ b/src/NuGetGallery/ViewModels/ListCertificateItemViewModel.cs @@ -2,12 +2,27 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using NuGetGallery.Helpers; namespace NuGetGallery { public sealed class ListCertificateItemViewModel { + /// + /// This value allows an abbreviated issuer and subject to show up on a single line in the web UI in the + /// "MD" Bootstrap screen size. This is approximately the P75 of CN length on NuGet.org. + /// + private const int AbbreviationLength = 36; + public string Sha1Thumbprint { get; } + public bool HasInfo { get; } + public bool IsExpired { get; } + public string ExpirationDisplay { get; } + public string ExpirationIso { get; } + public string Subject { get; } + public string Issuer { get; } + public string ShortSubject { get; } + public string ShortIssuer { get; } public bool CanDelete { get; } public string DeleteUrl { get; } @@ -19,6 +34,19 @@ public ListCertificateItemViewModel(Certificate certificate, string deleteUrl) } Sha1Thumbprint = certificate.Sha1Thumbprint; + HasInfo = certificate.Expiration.HasValue; + if (certificate.Expiration.HasValue) + { + // The value stored in the database is assumed to be UTC. + var expirationUtc = DateTime.SpecifyKind(certificate.Expiration.Value, DateTimeKind.Utc); + IsExpired = expirationUtc < DateTime.UtcNow; + ExpirationDisplay = expirationUtc.ToNuGetShortDateString(); + ExpirationIso = expirationUtc.ToString("O"); + } + Subject = certificate.Subject; + Issuer = certificate.Issuer; + ShortSubject = certificate.ShortSubject ?? certificate.Subject?.Abbreviate(AbbreviationLength); + ShortIssuer = certificate.ShortIssuer ?? certificate.Issuer?.Abbreviate(AbbreviationLength); CanDelete = !string.IsNullOrEmpty(deleteUrl); DeleteUrl = deleteUrl; } diff --git a/src/NuGetGallery/ViewModels/PackageViewModel.cs b/src/NuGetGallery/ViewModels/PackageViewModel.cs index 08fc7c7949..463eaaa52d 100644 --- a/src/NuGetGallery/ViewModels/PackageViewModel.cs +++ b/src/NuGetGallery/ViewModels/PackageViewModel.cs @@ -38,6 +38,8 @@ public PackageViewModel(Package package) ReleaseNotes = package.ReleaseNotes; IconUrl = package.IconUrl; ProjectUrl = package.ProjectUrl; + RepositoryUrl = package.RepositoryUrl; + RepositoryType = GetRepositoryKind(package.RepositoryUrl, package.RepositoryType); LicenseUrl = package.LicenseUrl; HideLicenseReport = package.HideLicenseReport; LatestVersion = package.IsLatest; @@ -62,6 +64,8 @@ public PackageViewModel(Package package) public string ReleaseNotes { get; set; } public string IconUrl { get; set; } public string ProjectUrl { get; set; } + public string RepositoryUrl { get; set; } + public RepositoryKind RepositoryType { get; private set; } public string LicenseUrl { get; set; } public Boolean HideLicenseReport { get; set; } public IEnumerable LicenseNames { get; set; } @@ -111,25 +115,56 @@ public PackageStatusSummary PackageStatusSummary switch (_packageStatus) { case PackageStatus.Validating: - { - return PackageStatusSummary.Validating; - } + { + return PackageStatusSummary.Validating; + } case PackageStatus.FailedValidation: - { - return PackageStatusSummary.FailedValidation; - } + { + return PackageStatusSummary.FailedValidation; + } case PackageStatus.Available: - { - return Listed ? PackageStatusSummary.Listed : PackageStatusSummary.Unlisted; - } + { + return Listed ? PackageStatusSummary.Listed : PackageStatusSummary.Unlisted; + } case PackageStatus.Deleted: - { - return PackageStatusSummary.None; - } + { + return PackageStatusSummary.None; + } default: throw new ArgumentOutOfRangeException(nameof(PackageStatus)); } } } + + private RepositoryKind GetRepositoryKind(string repositoryUrl, string repositoryType) + { + if (string.IsNullOrEmpty(repositoryUrl)) + { + return RepositoryKind.Unknown; + } + + if (Uri.TryCreate(repositoryUrl, UriKind.Absolute, out var repoUri)) + { + if ((string.Equals("http", repoUri.Scheme, StringComparison.Ordinal) || string.Equals("https", repoUri.Scheme, StringComparison.Ordinal)) + && (string.Equals(repoUri.Authority, "www.github.com", StringComparison.OrdinalIgnoreCase) || string.Equals(repoUri.Authority, "github.com", StringComparison.OrdinalIgnoreCase))) + { + return RepositoryKind.GitHub; + } + + if (string.Equals(repositoryType, "git", StringComparison.OrdinalIgnoreCase)) + { + return RepositoryKind.Git; + } + } + + return RepositoryKind.Unknown; + } + + public enum RepositoryKind + { + Unknown, + Git, + GitHub, + } } } \ No newline at end of file diff --git a/src/NuGetGallery/Views/CuratedFeeds/CuratedFeed.cshtml b/src/NuGetGallery/Views/CuratedFeeds/CuratedFeed.cshtml index 33c0ebab78..b5e7a591f5 100644 --- a/src/NuGetGallery/Views/CuratedFeeds/CuratedFeed.cshtml +++ b/src/NuGetGallery/Views/CuratedFeeds/CuratedFeed.cshtml @@ -1,7 +1,6 @@ @model CuratedFeedViewModel @{ ViewBag.Title = "Curated Feed: " + Model.Name; - ViewBag.MdPageColumns = Constants.ColumnsFormMd; Layout = "~/Views/Shared/Gallery/Layout.cshtml"; } @@ -9,7 +8,7 @@ @ViewHelpers.AjaxAntiForgeryToken(Html)
-
+

Curated Feed @Model.Name

diff --git a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml index 2012b686cc..3547b7cb58 100644 --- a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml +++ b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml @@ -1,3 +1,4 @@ + @using NuGet.Services.Validation; @model DisplayPackageViewModel @@ -471,7 +472,7 @@ - @packageVersion.FullVersion.Abbreviate(30) + @packageVersion.Version.Abbreviate(30) @if (packageVersion.IsCurrent(Model)) { @:(current) @@ -517,7 +518,7 @@ @VersionListDivider(rowCount, versionsExpanded) - @packageVersion.FullVersion (deleted) + @packageVersion.Version (deleted) @packageVersion.DownloadCount @@ -564,6 +565,31 @@ } + @if (!Model.Deleted && PackageHelper.ShouldRenderUrl(Model.RepositoryUrl, secureOnly: true)) + { +
  • + @switch (Model.RepositoryType) + { + case PackageViewModel.RepositoryKind.GitHub: + + break; + case PackageViewModel.RepositoryKind.Git: + + break; + default: + + break; + } + + + Source Code + +
  • + } @if (!Model.Deleted && PackageHelper.ShouldRenderUrl(Model.LicenseUrl)) {
  • diff --git a/src/NuGetGallery/Views/Packages/UploadPackage.cshtml b/src/NuGetGallery/Views/Packages/UploadPackage.cshtml index 7b19b780b5..5df65fa0b7 100644 --- a/src/NuGetGallery/Views/Packages/UploadPackage.cshtml +++ b/src/NuGetGallery/Views/Packages/UploadPackage.cshtml @@ -2,7 +2,6 @@ @{ ViewBag.Title = "Upload Package"; ViewBag.Tab = "Upload"; - ViewBag.MdPageColumns = Constants.ColumnsFormMd; Layout = "~/Views/Shared/Gallery/Layout.cshtml"; } @@ -28,8 +27,6 @@
    @Html.AntiForgeryToken() - @Html.ValidationSummary(true) -
  • - @if (Model != null && Model.IsUploadInProgress) {
    - @ViewHelpers.AlertWarning(@You had an upload in progress. You can continue it here or cancel to restart.) - @ViewHelpers.AlertPackageVerifyRecommendation() -
    - } - else - { - } + + @Html.Partial("_VerifyForm") diff --git a/src/NuGetGallery/Views/Packages/_VerifyMetadata.cshtml b/src/NuGetGallery/Views/Packages/_VerifyMetadata.cshtml index 1853bac86f..9a35b95496 100644 --- a/src/NuGetGallery/Views/Packages/_VerifyMetadata.cshtml +++ b/src/NuGetGallery/Views/Packages/_VerifyMetadata.cshtml @@ -44,144 +44,158 @@