Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #971 from bhuvak/staging

Merge from preview to staging
  • Loading branch information...
commit 2f6d9c0de3f317403adcf2dc104c5f93785bb0eb 2 parents cd4631c + 72998ea
@TimLovellSmith TimLovellSmith authored
Showing with 5,186 additions and 824 deletions.
  1. +20 −0 Changelog.md
  2. +0 −24 Facts/Controllers/ApiControllerFacts.cs
  3. +78 −23 Facts/Controllers/PackagesControllerFacts.cs
  4. +43 −4 Facts/Controllers/StatisticsControllerFacts.cs
  5. +1 −0  Facts/Controllers/UsersControllerFacts.cs
  6. +2 −2 Facts/Facts.csproj
  7. +2 −5 Facts/Infrastructure/LuceneSearchServiceFacts.cs
  8. +1 −1  Facts/Services/FeedServiceFacts.cs
  9. +67 −3 Facts/Services/MessageServiceFacts.cs
  10. +33 −0 Facts/TestUtility.cs
  11. +1 −1  Facts/packages.config
  12. +0 −4 Scripts/NuGetGallery.base.cscfg
  13. +13 −11 Scripts/NuGetGallery.csdef
  14. +0 −5 Scripts/NuGetGallery.emulator.cscfg
  15. +1 −25 Scripts/Startup.ps1
  16. +26 −9 Website/App_Code/ViewHelpers.cshtml
  17. +31 −2 Website/App_Start/AppActivator.cs
  18. +5 −0 Website/App_Start/Configuration.cs
  19. +2 −11 Website/App_Start/ContainerBindings.cs
  20. +1 −0  Website/App_Start/IConfiguration.cs
  21. +35 −26 Website/App_Start/Routes.cs
  22. +6 −2 Website/Content/Site.css
  23. +0 −5 Website/Controllers/ApiController.cs
  24. +109 −12 Website/Controllers/PackagesController.cs
  25. +5 −0 Website/Controllers/PagesController.cs
  26. +21 −2 Website/Controllers/StatisticsController.cs
  27. +1 −125 Website/DataServices/FeedServiceBase.cs
  28. +1 −1  Website/DataServices/PackageExtensions.cs
  29. +144 −0 Website/DataServices/SearchAdaptor.cs
  30. +1 −1  Website/DataServices/V1Feed.svc.cs
  31. +1 −1  Website/DataServices/V1FeedPackage.cs
  32. +3 −8 Website/DataServices/V2CuratedFeed.svc.cs
  33. +1 −1  Website/DataServices/V2Feed.svc.cs
  34. +4 −0 Website/Errors/Error.html
  35. +4 −0 Website/Errors/ErrorLayout.cshtml
  36. +0 −65 Website/Helpers/PackageHelper.cs
  37. +53 −0 Website/Infrastructure/Lucene/IntersectionFilter.cs
  38. +1 −1  Website/Infrastructure/Lucene/LuceneIndexingService.cs
  39. +10 −9 Website/Infrastructure/Lucene/LuceneSearchService.cs
  40. +57 −0 Website/Infrastructure/Lucene/PackageSetFilter.cs
  41. +22 −0 Website/PackagesController.generated.cs
  42. +7 −0 Website/PagesController.generated.cs
  43. +1 −0  Website/RouteNames.cs
  44. +3,583 −0 Website/Scripts/knockout-2.2.1.debug.js
  45. +85 −0 Website/Scripts/knockout-2.2.1.js
  46. +0 −87 Website/Scripts/knockout-latest.js
  47. +11 −4 Website/Services/CloudBlobClientWrapper.cs
  48. +0 −100 Website/Services/CloudPackageCacheService.cs
  49. +2 −2 Website/Services/CloudReportService.cs
  50. +2 −1  Website/Services/IMessageService.cs
  51. +0 −10 Website/Services/IPackageCacheService.cs
  52. +2 −2 Website/Services/ISearchService.cs
  53. +5 −2 Website/Services/IStatisticsService.cs
  54. +147 −31 Website/Services/JsonStatisticsService.cs
  55. +102 −21 Website/Services/MessageService.cs
  56. +0 −19 Website/Services/NullPackageCacheService.cs
  57. +1 −1  Website/Services/PackageService.cs
  58. +65 −0 Website/Services/ReportPackageRequest.cs
  59. +1 −0  Website/StatisticsController.generated.cs
  60. +3 −1 Website/T4MVC.cs
  61. +14 −3 Website/UrlExtensions.cs
  62. +1 −2  Website/ViewModels/DisplayPackageViewModel.cs
  63. +2 −0  Website/ViewModels/ListPackageItemViewModel.cs
  64. +43 −1 Website/ViewModels/ReportAbuseViewModel.cs
  65. +2 −0  Website/ViewModels/StatisticsPackagesItemViewModel.cs
  66. +13 −0 Website/ViewModels/StatisticsPackagesReport.cs
  67. +13 −14 Website/ViewModels/StatisticsPackagesViewModel.cs
  68. +0 −5 Website/Views/Authentication/LogOn.cshtml
  69. +0 −4 Website/Views/CuratedPackages/CreateCuratedPackageForm.cshtml
  70. +0 −5 Website/Views/Packages/ContactOwners.cshtml
  71. +14 −3 Website/Views/Packages/DisplayPackage.cshtml
  72. +40 −28 Website/Views/Packages/ManagePackageOwners.cshtml
  73. +21 −5 Website/Views/Packages/ReportAbuse.cshtml
  74. +47 −0 Website/Views/Packages/ReportMyPackage.cshtml
  75. +1 −0  Website/Views/Packages/UploadPackage.cshtml
  76. +9 −5 Website/Views/Packages/_ListPackage.cshtml
  77. +38 −0 Website/Views/Pages/Contact.cshtml
  78. +2 −1  Website/Views/Pages/Home.cshtml
  79. +7 −3 Website/Views/Shared/Layout.cshtml
  80. +1 −1  Website/Views/Statistics/Index.cshtml
  81. +5 −5 Website/Views/Statistics/PackageDownloadsByVersion.cshtml
  82. +37 −0 Website/Views/Statistics/PackageDownloadsDetail.cshtml
  83. +1 −1  Website/Views/Statistics/PackageVersions.cshtml
  84. +1 −0  Website/Views/Users/Account.cshtml
  85. +0 −5 Website/Views/Users/ChangePassword.cshtml
  86. +0 −5 Website/Views/Users/Edit.cshtml
  87. +0 −5 Website/Views/Users/ForgotPassword.cshtml
  88. +0 −5 Website/Views/Users/Register.cshtml
  89. +0 −5 Website/Views/Users/ResendConfirmation.cshtml
  90. +0 −5 Website/Views/Users/ResetPassword.cshtml
  91. +1 −0  Website/Views/web.config
  92. +21 −12 Website/Web.config
  93. +25 −25 Website/Website.csproj
  94. +4 −1 Website/packages.config
View
20 Changelog.md
@@ -0,0 +1,20 @@
+# Deployment 2013.03.28-CommitID #
+
+*Support for MinClientVersion*:
+
+ You can now upload packages with "[minclientVersion](http://nuget.codeplex.com/wikipage?title=NuGet%202.5%20list%20of%20features%20for%20Testing%20days%203%2f27%20to%203%2f29%20%2c%202013 )" to the NuGetGallery.
+ The minclientVersion of the package will shown in the package home page right next to the package description.
+
+*Contacting support*:
+
+ The "Report Abuse" page has been revamped to enable users to chose the specific issue with the package they are reporting. It also guides the user to differentiate between "Contact Owners" and "Report Abuse".
+
+*Improvised Package statistics*:
+
+ The package statistics now shows the break down of downloads based on the NuGet client (like NuGet CommandLine 2.1, NuGet Package Manager console 2.2 and so on and it also shows the split of the type of download operation (like Install, Restore, Update)
+
+*Other minor bug fixes*:
+
+ Complete list can be found here @ https://github.com/NuGet/NuGetGallery/issues?milestone=17
+
+
View
24 Facts/Controllers/ApiControllerFacts.cs
@@ -534,30 +534,6 @@ public class TheGetPackageAction
packageFileService.Verify();
packageService.Verify();
}
-
- [Fact]
- public async Task GetPackageReturnsRedirectResultWhenExternalPackageUrlIsNotNull()
- {
- var package = new Package { ExternalPackageUrl = "http://theUrl" };
- var packageService = new Mock<IPackageService>();
- packageService.Setup(x => x.FindPackageByIdAndVersion("thePackage", "42.1066", false)).Returns(package);
- var httpRequest = new Mock<HttpRequestBase>();
- httpRequest.SetupGet(r => r.UserHostAddress).Returns("Foo");
- httpRequest.SetupGet(r => r.UserAgent).Returns("Qux");
- NameValueCollection headers = new NameValueCollection();
- headers.Add("NuGet-Operation", "Install");
- httpRequest.SetupGet(r => r.Headers).Returns(headers);
- var httpContext = new Mock<HttpContextBase>();
- httpContext.SetupGet(c => c.Request).Returns(httpRequest.Object);
- var controller = CreateController(packageService);
- var controllerContext = new ControllerContext(new RequestContext(httpContext.Object, new RouteData()), controller);
- controller.ControllerContext = controllerContext;
-
- var result = await controller.GetPackage("thePackage", "42.1066") as RedirectResult;
-
- Assert.NotNull(result);
- Assert.Equal("http://theUrl", result.Url);
- }
}
public class ThePublishPackageAction
View
101 Facts/Controllers/PackagesControllerFacts.cs
@@ -109,7 +109,7 @@ private static Mock<ISearchService> CreateSearchService()
{
var searchService = new Mock<ISearchService>();
int total;
- searchService.Setup(s => s.Search(It.IsAny<IQueryable<Package>>(), It.IsAny<SearchFilter>(), out total)).Returns(
+ searchService.Setup(s => s.Search(It.IsAny<SearchFilter>(), out total, null)).Returns(
(IQueryable<Package> p, string searchTerm) => p);
return searchService;
@@ -395,10 +395,7 @@ public void SendsMessageToGalleryOwnerWithEmailOnlyWhenUnauthenticated()
{
var messageService = new Mock<IMessageService>();
messageService.Setup(
- s => s.ReportAbuse(
- It.IsAny<MailAddress>(),
- It.IsAny<Package>(),
- "Mordor took my finger"));
+ s => s.ReportAbuse(It.Is<ReportPackageRequest>(r => r.Message == "Mordor took my finger")));
var package = new Package
{
PackageRegistration = new PackageRegistration { Id = "mordor" },
@@ -415,18 +412,23 @@ public void SendsMessageToGalleryOwnerWithEmailOnlyWhenUnauthenticated()
var model = new ReportAbuseViewModel
{
Email = "frodo@hobbiton.example.com",
- Message = "Mordor took my finger."
+ Message = "Mordor took my finger.",
+ Reason = "GollumWasThere",
+ AlreadyContactedOwner = true,
};
+ TestUtility.SetupUrlHelper(controller, httpContext);
var result = controller.ReportAbuse("mordor", "2.0.1", model) as RedirectToRouteResult;
Assert.NotNull(result);
messageService.Verify(
s => s.ReportAbuse(
- It.Is<MailAddress>(m => m.Address == "frodo@hobbiton.example.com"),
- package,
- "Mordor took my finger."
- ));
+ It.Is<ReportPackageRequest>(
+ r => r.FromAddress.Address == "frodo@hobbiton.example.com"
+ && r.Package == package
+ && r.Reason == "GollumWasThere"
+ && r.Message == "Mordor took my finger."
+ && r.AlreadyContactedOwners)));
}
[Fact]
@@ -434,10 +436,7 @@ public void SendsMessageToGalleryOwnerWithUserInfoWhenAuthenticated()
{
var messageService = new Mock<IMessageService>();
messageService.Setup(
- s => s.ReportAbuse(
- It.IsAny<MailAddress>(),
- It.IsAny<Package>(),
- "Mordor took my finger"));
+ s => s.ReportAbuse(It.Is<ReportPackageRequest>(r => r.Message == "Mordor took my finger")));
var package = new Package
{
PackageRegistration = new PackageRegistration { Id = "mordor" },
@@ -449,7 +448,7 @@ public void SendsMessageToGalleryOwnerWithUserInfoWhenAuthenticated()
httpContext.Setup(h => h.Request.IsAuthenticated).Returns(true);
httpContext.Setup(h => h.User.Identity.Name).Returns("Frodo");
var userService = new Mock<IUserService>();
- userService.Setup(u => u.FindByUsername("Frodo")).Returns(new User { EmailAddress = "frodo@hobbiton.example.com", Username = "Frodo" });
+ userService.Setup(u => u.FindByUsername("Frodo")).Returns(new User { EmailAddress = "frodo@hobbiton.example.com", Username = "Frodo", Key = 1 });
var controller = CreateController(
packageService: packageService,
messageService: messageService,
@@ -457,21 +456,77 @@ public void SendsMessageToGalleryOwnerWithUserInfoWhenAuthenticated()
httpContext: httpContext);
var model = new ReportAbuseViewModel
{
- Message = "Mordor took my finger."
+ Message = "Mordor took my finger",
+ Reason = "GollumWasThere",
};
- var result = controller.ReportAbuse("mordor", "2.0.1", model) as RedirectToRouteResult;
+ TestUtility.SetupUrlHelper(controller, httpContext);
+ ActionResult result = controller.ReportAbuse("mordor", "2.0.1", model) as RedirectToRouteResult;
Assert.NotNull(result);
userService.VerifyAll();
messageService.Verify(
s => s.ReportAbuse(
- It.Is<MailAddress>(
- m => m.Address == "frodo@hobbiton.example.com"
- && m.DisplayName == "Frodo"),
- package,
- "Mordor took my finger."
- ));
+ It.Is<ReportPackageRequest>(
+ r => r.Message == "Mordor took my finger"
+ && r.FromAddress.Address == "frodo@hobbiton.example.com"
+ && r.FromAddress.DisplayName == "Frodo"
+ && r.Reason == "GollumWasThere")));
+ }
+
+ [Fact]
+ public void FormRedirectsPackageOwnerToReportMyPackage()
+ {
+ var package = new Package
+ {
+ PackageRegistration = new PackageRegistration { Id = "Mordor", Owners = { new User { Username = "Sauron" }} },
+ Version = "2.0.1"
+ };
+ var packageService = new Mock<IPackageService>();
+ packageService.Setup(p => p.FindPackageByIdAndVersion("Mordor", It.IsAny<string>(), true)).Returns(package);
+ var httpContext = new Mock<HttpContextBase>();
+ httpContext.Setup(h => h.Request.IsAuthenticated).Returns(true);
+ httpContext.Setup(h => h.User.Identity.Name).Returns("Sauron");
+ var userService = new Mock<IUserService>();
+ userService.Setup(u => u.FindByUsername("Sauron")).Returns(new User { EmailAddress = "darklord@mordor.com", Username = "Sauron" });
+ var controller = CreateController(
+ packageService: packageService,
+ userService: userService,
+ httpContext: httpContext);
+
+ TestUtility.SetupUrlHelper(controller, httpContext);
+ ActionResult result = controller.ReportAbuse("Mordor", "2.0.1");
+ Assert.IsType<RedirectToRouteResult>(result);
+ Assert.Equal("ReportMyPackage", ((RedirectToRouteResult)result).RouteValues["Action"]);
+ }
+ }
+
+ public class TheReportMyPackageMethod
+ {
+ [Fact]
+ public void FormRedirectsNonOwnersToReportAbuse()
+ {
+ var package = new Package
+ {
+ PackageRegistration = new PackageRegistration { Id = "Mordor", Owners = { new User { Username = "Sauron", Key = 1 } } },
+ Version = "2.0.1"
+ };
+ var packageService = new Mock<IPackageService>();
+ packageService.Setup(p => p.FindPackageByIdAndVersion("Mordor", It.IsAny<string>(), true)).Returns(package);
+ var httpContext = new Mock<HttpContextBase>();
+ httpContext.Setup(h => h.Request.IsAuthenticated).Returns(true);
+ httpContext.Setup(h => h.User.Identity.Name).Returns("Frodo");
+ var userService = new Mock<IUserService>();
+ userService.Setup(u => u.FindByUsername("Frodo")).Returns(new User { EmailAddress = "frodo@hobbiton.example.com", Username = "Frodo", Key = 2 });
+ var controller = CreateController(
+ packageService: packageService,
+ userService: userService,
+ httpContext: httpContext);
+
+ TestUtility.SetupUrlHelper(controller, httpContext);
+ ActionResult result = controller.ReportMyPackage("Mordor", "2.0.1");
+ Assert.IsType<RedirectToRouteResult>(result);
+ Assert.Equal("ReportAbuse", ((RedirectToRouteResult)result).RouteValues["Action"]);
}
}
View
47 Facts/Controllers/StatisticsControllerFacts.cs
@@ -151,11 +151,14 @@ public class StatisticsControllerFacts
{
string PackageId = "A";
- var fakeReport = "[{\"PackageVersion\":\"1.0\",\"Downloads\":101},{\"PackageVersion\":\"2.1\",\"Downloads\":202}]";
+ var fakeReport = "{\"Downloads\":303, Items:[{\"Version\":\"1.0\",\"Downloads\":101},{\"Version\":\"2.1\",\"Downloads\":202}]}";
var fakeReportService = new Mock<IReportService>();
- fakeReportService.Setup(x => x.Load("RecentPopularity_" + PackageId + ".json")).Returns(Task.FromResult(fakeReport));
+ string reportName = "RecentPopularityDetail_" + PackageId + ".json";
+ reportName = reportName.ToLowerInvariant();
+
+ fakeReportService.Setup(x => x.Load(reportName)).Returns(Task.FromResult(fakeReport));
var controller = new StatisticsController(new JsonStatisticsService(fakeReportService.Object));
@@ -163,13 +166,49 @@ public class StatisticsControllerFacts
int sum = 0;
- foreach (var item in model.PackageDownloadsByVersion)
+ foreach (var item in model.Report.Rows)
{
sum += item.Downloads;
}
Assert.Equal<int>(303, sum);
- Assert.Equal<int>(303, model.TotalPackageDownloads);
+ Assert.Equal<int>(303, model.Report.Total);
+ }
+
+ [Fact]
+ public async void Statistics_By_Client_Operation_ValidateReportStructureAndAvailability()
+ {
+ string PackageId = "A";
+ string PackageVersion = "2.1";
+
+ var fakeReport = "{\"Downloads\":303, Items:[";
+ fakeReport += "{\"Version\":\"1.0\", \"Downloads\":101},";
+ fakeReport += "{\"Version\":\"2.1\", \"Downloads\":70, Items:[";
+ fakeReport += "{\"Client\":\"Package Manager\", \"Operation\":\"Install\", Downloads:45},";
+ fakeReport += "{\"Client\":\"Package Manager\", \"Operation\":\"Restore\", Downloads:25},";
+ fakeReport += "]}";
+ fakeReport += "]}";
+
+ var fakeReportService = new Mock<IReportService>();
+
+ string reportName = "RecentPopularityDetail_" + PackageId + ".json";
+ reportName = reportName.ToLowerInvariant();
+
+ fakeReportService.Setup(x => x.Load(reportName)).Returns(Task.FromResult(fakeReport));
+
+ var controller = new StatisticsController(new JsonStatisticsService(fakeReportService.Object));
+
+ var model = (StatisticsPackagesViewModel)((ViewResult)await controller.PackageDownloadsDetail(PackageId, PackageVersion)).Model;
+
+ int sum = 0;
+
+ foreach (var item in model.Report.Rows)
+ {
+ sum += item.Downloads;
+ }
+
+ Assert.Equal<int>(70, sum);
+ Assert.Equal<int>(70, model.Report.Total);
}
[Fact]
View
1  Facts/Controllers/UsersControllerFacts.cs
@@ -17,6 +17,7 @@ public class UsersControllerFacts
Mock<IMessageService> messageService = null,
Mock<ICuratedFeedsByManagerQuery> feedsQuery = null,
Mock<IPrincipal> currentUser = null)
+
{
userService = userService ?? new Mock<IUserService>();
var packageService = new Mock<IPackageService>();
View
4 Facts/Facts.csproj
@@ -76,9 +76,9 @@
<Reference Include="MvcHaack.Ajax">
<HintPath>..\packages\MvcHaack.Ajax.MVC4.2.0.0.0\lib\net40\MvcHaack.Ajax.dll</HintPath>
</Reference>
- <Reference Include="NuGet.Core, Version=2.3.40210.45, Culture=neutral, processorArchitecture=MSIL">
+ <Reference Include="NuGet.Core, Version=2.3.40314.95, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
- <HintPath>..\packages\Nuget.Core.2.3.0-alpha002\lib\net40-Client\NuGet.Core.dll</HintPath>
+ <HintPath>..\packages\NuGet.Core.2.3.0-alpha003\lib\net40-Client\NuGet.Core.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
View
7 Facts/Infrastructure/LuceneSearchServiceFacts.cs
@@ -538,7 +538,6 @@ private IList<Package> IndexAndSearch(Mock<IPackageSource> packageSource, string
luceneIndexingService.UpdateIndex(forceRefresh: true);
var luceneSearchService = new LuceneSearchService(d);
- int totalHits = 0;
var searchFilter = new SearchFilter
{
Skip = 0,
@@ -546,10 +545,8 @@ private IList<Package> IndexAndSearch(Mock<IPackageSource> packageSource, string
SearchTerm = searchTerm,
};
- var results = luceneSearchService.Search(
- packageSource.Object.GetPackagesForIndexing(null),
- searchFilter,
- out totalHits).ToList();
+ int totalHits;
+ var results = luceneSearchService.Search(searchFilter, out totalHits).ToList();
return results;
}
View
2  Facts/Services/FeedServiceFacts.cs
@@ -74,7 +74,7 @@ public void V1FeedSearchDoesNotReturnPrereleasePackages()
configuration.Setup(c => c.GetSiteRoot(It.IsAny<bool>())).Returns("https://localhost:8081/");
var searchService = new Mock<ISearchService>(MockBehavior.Strict);
int total;
- searchService.Setup(s => s.Search(It.IsAny<IQueryable<Package>>(), It.IsAny<SearchFilter>(), out total)).Returns
+ searchService.Setup(s => s.Search(It.IsAny<SearchFilter>(), out total, null)).Returns
<IQueryable<Package>, string>((_, __) => _);
var v1Service = new TestableV1Feed(repo.Object, configuration.Object, searchService.Object);
View
70 Facts/Services/MessageServiceFacts.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.ObjectModel;
using System.Net.Mail;
using AnglicanGeek.MarkdownMailer;
using Moq;
@@ -27,12 +28,75 @@ public void WillSendEmailToGalleryOwner()
MailMessage message = null;
mailSender.Setup(m => m.Send(It.IsAny<MailMessage>())).Callback<MailMessage>(m => { message = m; });
- messageService.ReportAbuse(from, package, "Abuse!");
+ messageService.ReportAbuse(
+ new ReportPackageRequest
+ {
+ AlreadyContactedOwners = true,
+ FromAddress = from,
+ Message = "Abuse!",
+ Package = package,
+ Reason = "Reason!",
+ RequestingUser = null,
+ Url = TestUtility.MockUrlHelper(),
+ });
+
+ Assert.Equal("joe@example.com", message.To[0].Address);
+ Assert.Equal("[NuGet Gallery] Support Request for 'smangit' version 1.42.0.1 (Reason: Reason!)", message.Subject);
+ Assert.Contains("Reason!", message.Body);
+ Assert.Contains("Abuse!", message.Body);
+ Assert.Contains("too (legit@example.com)", message.Body);
+ Assert.Contains("smangit", message.Body);
+ Assert.Contains("1.42.0.1", message.Body);
+ Assert.Contains("Yes", message.Body);
+ }
+ }
+
+ public class TheReportMyPackageMethod
+ {
+ [Fact]
+ public void WillSendEmailToGalleryOwner()
+ {
+ var from = new MailAddress("legit@example.com", "too");
+ var owner = new User
+ {
+ Username = "too",
+ EmailAddress = "legit@example.com",
+ };
+ var package = new Package
+ {
+ PackageRegistration = new PackageRegistration
+ {
+ Id = "smangit",
+ Owners = new Collection<User> { owner }
+ },
+ Version = "1.42.0.1"
+ };
+ var mailSender = new Mock<IMailSender>();
+ var config = new Mock<IConfiguration>();
+ config.Setup(x => x.GalleryOwnerName).Returns("NuGet Gallery");
+ config.Setup(x => x.GalleryOwnerEmail).Returns("joe@example.com");
+ var messageService = new MessageService(mailSender.Object, config.Object);
+ MailMessage message = null;
+ mailSender.Setup(m => m.Send(It.IsAny<MailMessage>())).Callback<MailMessage>(m => { message = m; });
+
+ messageService.ReportMyPackage(
+ new ReportPackageRequest
+ {
+ FromAddress = from,
+ Message = "Abuse!",
+ Package = package,
+ Reason = "Reason!",
+ RequestingUser = owner,
+ Url = TestUtility.MockUrlHelper(),
+ });
Assert.Equal("joe@example.com", message.To[0].Address);
- Assert.Equal("[NuGet Gallery] Abuse Report for Package 'smangit' Version '1.42.0.1'", message.Subject);
+ Assert.Equal("[NuGet Gallery] Owner Support Request for 'smangit' version 1.42.0.1 (Reason: Reason!)", message.Subject);
+ Assert.Contains("Reason!", message.Body);
Assert.Contains("Abuse!", message.Body);
- Assert.Contains("User too (legit@example.com) reports the package 'smangit' version '1.42.0.1' as abusive", message.Body);
+ Assert.Contains("too (legit@example.com)", message.Body);
+ Assert.Contains("smangit", message.Body);
+ Assert.Contains("1.42.0.1", message.Body);
}
}
View
33 Facts/TestUtility.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Specialized;
using System.Reflection;
using System.Threading.Tasks;
using System.Web;
@@ -26,6 +27,38 @@ public static Mock<HttpContextBase> SetupHttpContextMockForUrlGeneration(Mock<Ht
return httpContext;
}
+ public static void SetupUrlHelper(Controller controller, Mock<HttpContextBase> mockHttpContext)
+ {
+ var routes = new RouteCollection();
+ Routes.RegisterRoutes(routes);
+ controller.Url = new UrlHelper(new RequestContext(mockHttpContext.Object, new RouteData()), routes);
+ }
+
+ public static UrlHelper MockUrlHelper()
+ {
+ var mockHttpContext = new Mock<HttpContextBase>(MockBehavior.Strict);
+ var mockHttpRequest = new Mock<HttpRequestBase>(MockBehavior.Strict);
+ var mockHttpResponse = new Mock<HttpResponseBase>(MockBehavior.Strict);
+ mockHttpContext.Setup(httpContext => httpContext.Request).Returns(mockHttpRequest.Object);
+ mockHttpContext.Setup(httpContext => httpContext.Response).Returns(mockHttpResponse.Object);
+ mockHttpRequest.Setup(httpRequest => httpRequest.Url).Returns(new Uri("http://unittest.nuget.org/"));
+ mockHttpRequest.Setup(httpRequest => httpRequest.ApplicationPath).Returns("http://unittest.nuget.org/");
+ mockHttpRequest.Setup(httpRequest => httpRequest.ServerVariables).Returns(new NameValueCollection());
+
+ string value = null;
+ Action<string> saveValue = x =>
+ {
+ value = x;
+ };
+ Func<String> restoreValue = () => value;
+ mockHttpResponse.Setup(httpResponse => httpResponse.ApplyAppPathModifier(It.IsAny<string>()))
+ .Callback(saveValue).Returns(restoreValue);
+ var requestContext = new RequestContext(mockHttpContext.Object, new RouteData());
+ var routes = new RouteCollection();
+ Routes.RegisterRoutes(routes);
+ return new UrlHelper(requestContext, routes);
+ }
+
public static T GetAnonymousPropertyValue<T>(Object source, string propertyName)
{
var property = source.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public);
View
2  Facts/packages.config
@@ -13,7 +13,7 @@
<package id="Microsoft.WindowsAzure.ConfigurationManager" version="1.7.0.3" targetFramework="net45" />
<package id="Moq" version="4.0.10827" />
<package id="MvcHaack.Ajax.MVC4" version="2.0.0.0" targetFramework="net45" />
- <package id="Nuget.Core" version="2.3.0-alpha002" targetFramework="net45" />
+ <package id="NuGet.Core" version="2.3.0-alpha003" targetFramework="net45" />
<package id="System.Spatial" version="5.1.0" targetFramework="net45" />
<package id="WebActivator" version="1.5" />
<package id="WebBackgrounder" version="0.2.0" targetFramework="net40" />
View
4 Scripts/NuGetGallery.base.cscfg
@@ -45,10 +45,6 @@
<!-- Set this to use the Azure CDN -->
<Setting name="Gallery.AzureCdnHost" value="" />
- <!-- Make sure these are unique, secret and remain constant for a particular instance of the gallery! Otherwise cookies will break -->
- <Setting name="Gallery.ValidationKey" value="" />
- <Setting name="Gallery.DecryptionKey" value="" />
-
<!-- Provide RECAPTCHA keys for your own domain. https://www.google.com/recaptcha/admin/create -->
<Setting name="reCAPTCHA.PrivateKey" value="" />
<Setting name="reCAPTCHA.PublicKey" value="" />
View
24 Scripts/NuGetGallery.csdef
@@ -11,7 +11,6 @@
<Setting name="Gallery.AzureDiagnosticsConnectionString" />
<Setting name="Gallery.AzureStatisticsConnectionString" />
<Setting name="Gallery.Sql.NuGetGallery" />
-
<!-- SMTP Settings -->
<Setting name="Gallery.GalleryOwnerName" />
<Setting name="Gallery.GalleryOwnerEmail" />
@@ -21,29 +20,22 @@
<Setting name="Gallery.SmtpPort" />
<Setting name="Gallery.UseSmtp" />
<Setting name="Gallery.ConfirmEmailAddresses" />
-
<!-- Basic Site Configuration -->
<Setting name="Gallery.PackageStoreType" />
- <Setting name="Gallery.ReadOnlyMode" /><!-- set value 'true' to put the gallery in read only mode -->
+ <Setting name="Gallery.ReadOnlyMode" />
+ <!-- set value 'true' to put the gallery in read only mode -->
<Setting name="Gallery.SiteRoot" />
<Setting name="Gallery.SSL.Required" />
-
+ <Setting name="Gallery.HasWorker" />
<!-- Set this to enable Google Analytics -->
<Setting name="Gallery.GoogleAnalyticsPropertyId" />
-
<!-- Set this to enable Facebook Buttons with a specific App ID -->
<Setting name="Gallery.FacebookAppId" />
-
<!-- Set this to enable use of the Azure Caching Service for Package Explorer in the Cloud -->
<Setting name="Gallery.AzureCacheEndpoint" />
<Setting name="Gallery.AzureCacheKey" />
-
<!-- Set this to use the Azure CDN -->
<Setting name="Gallery.AzureCdnHost" />
-
- <!-- Make sure these are unique, secret and remain constant for a particular instance of the gallery! Otherwise cookies will break -->
- <Setting name="Gallery.ValidationKey" />
- <Setting name="Gallery.DecryptionKey" />
<Setting name="reCAPTCHA.PrivateKey" />
<Setting name="reCAPTCHA.PublicKey" />
</ConfigurationSettings>
@@ -58,6 +50,16 @@
<Endpoints>
<InputEndpoint name="Non-SSL" protocol="http" port="80" />
<InputEndpoint name="SSL" protocol="https" port="443" certificate="sslcertificate" />
+ <InstanceInputEndpoint name="InstanceDirectNonSSL" protocol="tcp" localPort="80">
+ <AllocatePublicPortFrom>
+ <FixedPortRange min="81" max="82" />
+ </AllocatePublicPortFrom>
+ </InstanceInputEndpoint>
+ <InstanceInputEndpoint name="InstanceDirectSSL" protocol="tcp" localPort="443">
+ <AllocatePublicPortFrom>
+ <FixedPortRange min="44301" max="44302" />
+ </AllocatePublicPortFrom>
+ </InstanceInputEndpoint>
</Endpoints>
<LocalResources>
<LocalStorage name="IISLogs" sizeInMB="128" cleanOnRoleRecycle="false" />
View
5 Scripts/NuGetGallery.emulator.cscfg
@@ -25,11 +25,6 @@
<!-- Set this to enable use of the Azure Caching Service for Package Explorer in the Cloud -->
<Setting name="Gallery.AzureCacheEndpoint" value="" />
<Setting name="Gallery.AzureCacheKey" value="" />
-
- <!-- Make sure these are unique, secret and remain constant for a particular instance of the gallery! Otherwise cookies will break -->
- <!-- These are just some values for the Emulated environment. Don't use them in production! They are public! -->
- <Setting name="Gallery.ValidationKey" value="536AE50FF6EEEF5E6D3AE9C1A7D40EBB6A89ED7A6670956BE8CF48482CC985F39C07129C0044AFB8DD719FD17B9382BB64BC09D1315ABA3EB011C03B614F1474" />
- <Setting name="Gallery.DecryptionKey" value="8EAE82EF44953302DF848E53C2AB75EC3C77F5B71BA7D59ADDABCDBB8907153A" />
</ConfigurationSettings>
<Certificates>
<Certificate name="Microsoft.WindowsAzure.Plugins.RemoteAccess.PasswordEncryption" thumbprint="123" thumbprintAlgorithm="sha1" />
View
26 Scripts/Startup.ps1
@@ -1,27 +1,3 @@
# Enable Dynamic HTTP Compression for application/* mime types.
& "$env:windir\system32\inetsrv\appcmd.exe" set config /section:urlCompression /doDynamicCompression:True /commit:apphost
-& "$env:windir\system32\inetsrv\appcmd.exe" set config /section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/*',enabled='True']" /commit:apphost
-
-# Load Azure Service Runtime Assembly
-[Reflection.Assembly]::LoadWithPartialName("Microsoft.WindowsAzure.ServiceRuntime")
-
-# Load machine keys from service config
-$validationKey = [Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::GetConfigurationSettingValue("Gallery.ValidationKey");
-$decryptionKey = [Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::GetConfigurationSettingValue("Gallery.DecryptionKey");
-
-# Push the keys in to the web.config
-function set-machinekey {
- param($path)
- if($validationKey -AND $decryptionKey){
- $xml = [xml](get-content $path)
- $machinekey = $xml.CreateElement("machineKey")
- $machinekey.setattribute("validation", "HMACSHA256")
- $machinekey.setattribute("validationKey", $validationKey)
- $machinekey.setattribute("decryption", "AES")
- $machinekey.setattribute("decryptionKey", $decryptionKey)
- $xml.configuration."system.web".AppendChild($machineKey)
- $resolvedPath = (resolve-path $path)
- $xml.save($resolvedPath)
- }
-}
-set-machinekey (Convert-Path "..\Web.Config")
+& "$env:windir\system32\inetsrv\appcmd.exe" set config /section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/*',enabled='True']" /commit:apphost
View
35 Website/App_Code/ViewHelpers.cshtml
@@ -1,6 +1,7 @@
@using System.Configuration
@using System.Web.Mvc
@using Microsoft.Web.Helpers
+@using Microsoft.WindowsAzure.ServiceRuntime
@using NuGetGallery
@using Ninject
@@ -107,7 +108,7 @@
@helper OwnerGravatar(User owner, int size, UrlHelper url, bool showName = true)
{
- <a class="owner" href="@url.Action(MVC.Users.Profiles(owner.Username))" title="@owner.Username">
+ <a class="owner" href="@url.User(owner)" title="@owner.Username">
@if (!String.IsNullOrEmpty(owner.EmailAddress))
{
@GravatarImage(owner.EmailAddress, owner.Username, size)
@@ -134,14 +135,30 @@
string sha = ConfigurationManager.AppSettings["Gallery.ReleaseSha"];
string branch = ConfigurationManager.AppSettings["Gallery.ReleaseBranch"];
string time = ConfigurationManager.AppSettings["Gallery.ReleaseTime"];
- if (!String.IsNullOrEmpty(sha) && !String.IsNullOrEmpty(branch) && !String.IsNullOrEmpty(time))
- {
- <p id="releaseTag">
- Deployed from <a href="https://github.com/NuGet/NuGetGallery/commit/@sha" title="View the commit.">@sha.Substring(0, Math.Min(sha.Length, 10))</a>.
- Originally built on <a href="https://github.com/NuGet/NuGetGallery/branches/@branch" title="View the branch.">@branch</a>
- at @time.
- </p>
- }
+ <p id="releaseTag">
+ This is the NuGet Gallery.
+ @if (!String.IsNullOrEmpty(sha) && !String.IsNullOrEmpty(branch) && !String.IsNullOrEmpty(time))
+ {
+ <text>
+ Deployed from <a href="https://github.com/NuGet/NuGetGallery/commit/@sha" title="View the commit.">@sha.Substring(0, Math.Min(sha.Length, 10))</a>.
+ Originally built on <a href="https://github.com/NuGet/NuGetGallery/branches/@branch" title="View the branch.">@branch</a>
+ at @time.
+ </text>
+ }
+
+ @* A little quick-n-dirty code to display the current machine *@
+ @* In Azure, we want the Instance ID. The Machine Name is total garbage *@
+ @try {
+ if(RoleEnvironment.IsAvailable) {
+ <text>You are on @RoleEnvironment.CurrentRoleInstance.Id.</text>
+ } else {
+ <text>You are on @Environment.MachineName.</text>
+ }
+ } catch(Exception) {
+ @* Azure SDK not installed so we can't even run RoleEnvironment.IsAvailable. Just use Machine Name *@
+ <text>You are on @Environment.MachineName.</text>
+ }
+ </p>
}
@helper AnalyticsScript()
View
33 Website/App_Start/AppActivator.cs
@@ -3,6 +3,7 @@
using System.Linq;
using System.Web;
using System.Web.Mvc;
+using System.Web.Optimization;
using System.Web.Routing;
using DynamicDataEFCodeFirst;
using Elmah;
@@ -47,6 +48,7 @@ public static void PostStart()
BackgroundJobsPostStart(config);
AppPostStart();
DynamicDataPostStart(config);
+ BundlingPostStart();
}
public static void Stop()
@@ -55,6 +57,26 @@ public static void Stop()
NinjectStop();
}
+ private static void BundlingPostStart()
+ {
+ var scriptBundle = new ScriptBundle("~/bundles/js")
+ .Include("~/Scripts/jquery-{version}.js")
+ .Include("~/Scripts/jquery.validate.js")
+ .Include("~/Scripts/jquery.validate.unobtrusive.js");
+ BundleTable.Bundles.Add(scriptBundle);
+
+ // Modernizr needs to be delivered at the top of the page but putting it in a bundle gets us a cache-buster.
+ // TODO: Use minified modernizr!
+ var modernizrBundle = new ScriptBundle("~/bundles/modernizr")
+ .Include("~/Scripts/modernizr-2.0.6-development-only.js");
+ BundleTable.Bundles.Add(modernizrBundle);
+
+ var stylesBundle = new StyleBundle("~/bundles/css")
+ .Include("~/Content/site.css");
+ BundleTable.Bundles.Add(stylesBundle);
+
+ }
+
private static void ElmahPreStart()
{
ServiceCenter.Current = _ => Container.Kernel;
@@ -63,6 +85,7 @@ private static void ElmahPreStart()
private static void AppPostStart()
{
Routes.RegisterRoutes(RouteTable.Routes);
+ Routes.RegisterServiceRoutes(RouteTable.Routes);
GlobalFilters.Filters.Add(new ElmahHandleErrorAttribute());
GlobalFilters.Filters.Add(new ReadOnlyModeErrorFilter());
GlobalFilters.Filters.Add(new RequireRemoteHttpsAttribute() { OnlyWhenAuthenticated = true });
@@ -71,7 +94,13 @@ private static void AppPostStart()
private static void BackgroundJobsPostStart(IConfiguration configuration)
{
- var jobs = new IJob[]
+ var jobs = configuration.HasWorker ?
+ new IJob[]
+ {
+ new LuceneIndexingJob(TimeSpan.FromMinutes(10), () => new EntitiesContext(configuration.SqlConnectionString, readOnly: true), timeout: TimeSpan.FromMinutes(2))
+ }
+ :
+ new IJob[]
{
// readonly: false workaround - let statistics background job write to DB in read-only mode since we don't care too much about losing that data
new UpdateStatisticsJob(TimeSpan.FromMinutes(5),
@@ -101,7 +130,7 @@ private static void DbMigratorPostStart()
// To make app startup not directly depend on the database,
// we set the migrations to run when the database is first used, instead of doing it up-front.
- Database.SetInitializer(new MigrateDatabaseToLatestVersion<EntitiesContext,MigrationsConfiguration>());
+ Database.SetInitializer(new MigrateDatabaseToLatestVersion<EntitiesContext, MigrationsConfiguration>());
}
private static void DynamicDataPostStart(IConfiguration configuration)
View
5 Website/App_Start/Configuration.cs
@@ -21,6 +21,11 @@ public Configuration()
_httpsSiteRootThunk = new Lazy<string>(GetHttpsSiteRoot);
}
+ public bool HasWorker
+ {
+ get { return ReadAppSettings("HasWorker", str => Boolean.Parse(str ?? "true")); }
+ }
+
public string EnvironmentName
{
get { return ReadAppSettings("Environment") ?? "Development"; }
View
13 Website/App_Start/ContainerBindings.cs
@@ -51,11 +51,6 @@ public override void Load()
if (IsDeployedToCloud)
{
- // when running on Windows Azure, use the Azure Cache local storage
- Bind<IPackageCacheService>()
- .To<CloudPackageCacheService>()
- .InSingletonScope();
-
// when running on Windows Azure, use the Azure Cache service if available
if (!String.IsNullOrEmpty(configuration.AzureCacheEndpoint))
{
@@ -81,10 +76,6 @@ public override void Load()
}
else
{
- Bind<IPackageCacheService>()
- .To<NullPackageCacheService>()
- .InSingletonScope();
-
// when running locally on dev box, use the built-in ASP.NET Http Cache
Bind<ICacheService>()
.To<HttpContextCacheService>()
@@ -203,10 +194,10 @@ public override void Load()
.To<FileSystemFileStorageService>()
.InSingletonScope();
break;
- case PackageStoreType.AzureStorageBlob:
+ case PackageStoreType.AzureStorageBlob:
Bind<ICloudBlobClient>()
.ToMethod(
- context => new CloudBlobClientWrapper(CloudStorageAccount.Parse(configuration.AzureStorageConnectionString).CreateCloudBlobClient()))
+ _ => new CloudBlobClientWrapper(configuration.AzureStorageConnectionString))
.InSingletonScope();
Bind<IFileStorageService>()
.To<CloudBlobFileStorageService>()
View
1  Website/App_Start/IConfiguration.cs
@@ -2,6 +2,7 @@
{
public interface IConfiguration
{
+ bool HasWorker { get; }
bool RequireSSL { get; }
int SSLPort { get; }
View
61 Website/App_Start/Routes.cs
@@ -35,10 +35,15 @@ public static void RegisterRoutes(RouteCollection routes)
new { controller = MVC.Statistics.Name, action = "PackageVersions" });
routes.MapRoute(
+ RouteName.StatisticsPackageDownloadsDetail,
+ "stats/packages/{id}/{version}",
+ new { controller = MVC.Statistics.Name, action = "PackageDownloadsDetail" });
+
+ routes.MapRoute(
RouteName.StatisticsPackageDownloadsByVersion,
"stats/packages/{id}",
new { controller = MVC.Statistics.Name, action = "PackageDownloadsByVersion" });
-
+
routes.Add(new JsonRoute("json/{controller}"));
routes.MapRoute(
@@ -150,16 +155,6 @@ public static void RegisterRoutes(RouteCollection routes)
// TODO : Most of the routes are essentially of the format api/v{x}/*. We should refactor the code to vary them by the version.
// V1 Routes
// If the push url is /api/v1 then NuGet.Core would ping the path to resolve redirection.
- routes.MapServiceRoute(
- RouteName.V1ApiFeed,
- "api/v1/FeedService.svc",
- typeof(V1Feed));
-
- routes.MapServiceRoute(
- "LegacyFeedService",
- "v1/FeedService.svc",
- typeof(V1Feed));
-
routes.MapRoute(
"v1" + RouteName.VerifyPackageKey,
"api/v1/verifykey/{id}/{version}",
@@ -188,11 +183,6 @@ public static void RegisterRoutes(RouteCollection routes)
"v1/PublishedPackages/Publish",
MVC.Api.PublishPackage());
- routes.MapServiceRoute(
- "v1" + RouteName.V1ApiFeed,
- "api/v1",
- typeof(V1Feed));
-
// V2 routes
routes.MapRoute(
"v2" + RouteName.VerifyPackageKey,
@@ -242,16 +232,6 @@ public static void RegisterRoutes(RouteCollection routes)
"api/v2/package-versions/{id}",
MVC.Api.GetPackageVersions());
- routes.MapServiceRoute(
- RouteName.V2ApiCuratedFeed,
- "api/v2/curated-feed",
- typeof(V2CuratedFeed));
-
- routes.MapServiceRoute(
- RouteName.V2ApiFeed,
- "api/v2/",
- typeof(V2Feed));
-
routes.MapRoute(
RouteName.DownloadNuGetExe,
"nuget.exe",
@@ -314,5 +294,34 @@ public static void RegisterRoutes(RouteCollection routes)
new { controller = MVC.Api.Name, action = "GetPackageApi", version = UrlParameter.Optional }),
permanent: true).To(downloadRoute);
}
+
+ // note: Pulled out service route registration separately because it's not testable T.T (won't run outside IIS/WAS)
+ public static void RegisterServiceRoutes(RouteCollection routes)
+ {
+ routes.MapServiceRoute(
+ RouteName.V1ApiFeed,
+ "api/v1/FeedService.svc",
+ typeof(V1Feed));
+
+ routes.MapServiceRoute(
+ "LegacyFeedService",
+ "v1/FeedService.svc",
+ typeof(V1Feed));
+
+ routes.MapServiceRoute(
+ "v1" + RouteName.V1ApiFeed,
+ "api/v1",
+ typeof(V1Feed));
+
+ routes.MapServiceRoute(
+ RouteName.V2ApiCuratedFeed,
+ "api/v2/curated-feed",
+ typeof(V2CuratedFeed));
+
+ routes.MapServiceRoute(
+ RouteName.V2ApiFeed,
+ "api/v2/",
+ typeof(V2Feed));
+ }
}
}
View
8 Website/Content/Site.css
@@ -791,6 +791,10 @@ section.package {
padding-top: 10px;
}
+section.package div.minimum-client-version {
+ float: right;
+}
+
section.package.my-package { background: url("Images/yourpackage.png") no-repeat 100% 0; }
section.package .main { margin-left: 70px; }
@@ -1054,6 +1058,7 @@ fieldset.form legend { display: none; }
}
.form-field textarea,
+.form-field input[type="email"],
.form-field input[type="text"],
.form-field input[type="file"],
.form-field input[type="password"] {
@@ -1199,7 +1204,7 @@ input:focus ~ .field-validation-error ~ .field-hint-message { display: none; }
footer#footer {
margin: 0 auto;
text-align: center;
- width: 80%;
+ display: table;
}
footer#footer p {
@@ -1220,7 +1225,6 @@ footer#footer ul.recommended {
list-style: none;
margin: 0 auto;
padding: 0;
- width: 760px;
}
footer#footer ul.recommended li {
View
5 Website/Controllers/ApiController.cs
@@ -88,11 +88,6 @@ public partial class ApiController : AppController
QuietlyLogException(e);
}
- if (!String.IsNullOrWhiteSpace(package.ExternalPackageUrl))
- {
- return Redirect(package.ExternalPackageUrl);
- }
-
return await _packageFileService.CreateDownloadPackageActionResultAsync(HttpContext.Request.Url, package);
}
catch (SqlException e)
View
121 Website/Controllers/PackagesController.cs
@@ -179,8 +179,6 @@ public virtual ActionResult ListPackages(string q, string sortOrder = null, int
page = 1;
}
- IQueryable<Package> packageVersions = _packageService.GetPackagesForListing(prerelease);
-
q = (q ?? "").Trim();
if (String.IsNullOrEmpty(sortOrder))
@@ -192,7 +190,7 @@ public virtual ActionResult ListPackages(string q, string sortOrder = null, int
var searchFilter = GetSearchFilter(q, sortOrder, page, prerelease);
int totalHits;
- packageVersions = _searchService.Search(packageVersions, searchFilter, out totalHits);
+ IQueryable<Package> packageVersions = _searchService.Search(searchFilter, out totalHits);
if (page == 1 && !packageVersions.Any())
{
// In the event the index wasn't updated, we may get an incorrect count.
@@ -225,20 +223,72 @@ public virtual ActionResult ReportAbuse(string id, string version)
}
var model = new ReportAbuseViewModel
+ {
+ ReasonChoices =
{
- PackageId = id,
- PackageVersion = package.Version,
- };
+ ReportPackageReason.IsFraudulent,
+ ReportPackageReason.ViolatesALicenseIOwn,
+ ReportPackageReason.ContainsMaliciousCode,
+ ReportPackageReason.HasABug,
+ ReportPackageReason.Other
+ },
+ PackageId = id,
+ PackageVersion = package.Version,
+ };
if (Request.IsAuthenticated)
{
var user = _userService.FindByUsername(HttpContext.User.Identity.Name);
+
+ // If user logged on in as owner a different tab, then clicked the link, we can redirect them to ReportMyPackage
+ if (package.IsOwner(user))
+ {
+ return RedirectToAction(ActionNames.ReportMyPackage, new {id, version});
+ }
+
if (user.Confirmed)
{
model.ConfirmedUser = true;
}
}
+ ViewData[Constants.ReturnUrlViewDataKey] = Url.Action(ActionNames.ReportMyPackage, new {id, version});
+ return View(model);
+ }
+
+ [Authorize]
+ public virtual ActionResult ReportMyPackage(string id, string version)
+ {
+ var user = _userService.FindByUsername(HttpContext.User.Identity.Name);
+
+ var package = _packageService.FindPackageByIdAndVersion(id, version);
+
+ if (package == null)
+ {
+ return HttpNotFound();
+ }
+
+ // If user hit this url by constructing it manually but is not the owner, redirect them to ReportAbuse
+ if (!(HttpContext.User.IsInRole(Constants.AdminRoleName) || package.IsOwner(user)))
+ {
+ return RedirectToAction(ActionNames.ReportAbuse, new { id, version });
+ }
+
+ var model = new ReportAbuseViewModel
+ {
+ ReasonChoices =
+ {
+ ReportPackageReason.ContainsPrivateAndConfidentialData,
+ ReportPackageReason.PublishedWithWrongVersion,
+ ReportPackageReason.ReleasedInPublicByAccident,
+ ReportPackageReason.ContainsMaliciousCode,
+ ReportPackageReason.Other
+ },
+ ConfirmedUser = user.Confirmed,
+ PackageId = id,
+ PackageVersion = package.Version,
+ };
+
return View(model);
}
@@ -258,10 +308,11 @@ public virtual ActionResult ReportAbuse(string id, string version, ReportAbuseVi
return HttpNotFound();
}
+ User user = null;
MailAddress from;
if (Request.IsAuthenticated)
{
- var user = _userService.FindByUsername(HttpContext.User.Identity.Name);
+ user = _userService.FindByUsername(HttpContext.User.Identity.Name);
from = user.ToMailAddress();
}
else
@@ -269,12 +320,58 @@ public virtual ActionResult ReportAbuse(string id, string version, ReportAbuseVi
from = new MailAddress(reportForm.Email);
}
- _messageService.ReportAbuse(from, package, reportForm.Message);
+ var request = new ReportPackageRequest
+ {
+ AlreadyContactedOwners = reportForm.AlreadyContactedOwner,
+ FromAddress = from,
+ Message = reportForm.Message,
+ Package = package,
+ Reason = reportForm.Reason,
+ RequestingUser = user,
+ Url = Url
+ };
+ _messageService.ReportAbuse(request
+ );
TempData["Message"] = "Your abuse report has been sent to the gallery operators.";
return RedirectToAction(MVC.Packages.DisplayPackage(id, version));
}
+ [HttpPost]
+ [Authorize]
+ [ValidateAntiForgeryToken]
+ [ValidateSpamPrevention]
+ public virtual ActionResult ReportMyPackage(string id, string version, ReportAbuseViewModel reportForm)
+ {
+ if (!ModelState.IsValid)
+ {
+ return ReportMyPackage(id, version);
+ }
+
+ var package = _packageService.FindPackageByIdAndVersion(id, version);
+ if (package == null)
+ {
+ return HttpNotFound();
+ }
+
+ var user = _userService.FindByUsername(HttpContext.User.Identity.Name);
+ MailAddress from = user.ToMailAddress();
+
+ _messageService.ReportMyPackage(
+ new ReportPackageRequest
+ {
+ FromAddress = from,
+ Message = reportForm.Message,
+ Package = package,
+ Reason = reportForm.Reason,
+ RequestingUser = user,
+ Url = Url
+ });
+
+ TempData["Message"] = "Your support request has been sent to the gallery operators.";
+ return RedirectToAction(MVC.Packages.DisplayPackage(id, version));
+ }
+
[Authorize]
public virtual ActionResult ContactOwners(string id)
{
@@ -555,9 +652,6 @@ private ActionResult GetPackageOwnerActionFormResult(string id, string version)
// tell Lucene to update index for the new package
_indexingService.UpdateIndex();
- // delete the uploaded binary in the Uploads container
- await _uploadFileService.DeleteUploadFileAsync(currentUser.Key);
-
// If we're pushing a new stable version of NuGet.CommandLine, update the extracted executable.
if (package.PackageRegistration.Id.Equals(Constants.NuGetCommandLinePackageId, StringComparison.OrdinalIgnoreCase) &&
package.IsLatestStable)
@@ -566,6 +660,9 @@ private ActionResult GetPackageOwnerActionFormResult(string id, string version)
}
}
+ // delete the uploaded binary in the Uploads container
+ await _uploadFileService.DeleteUploadFileAsync(currentUser.Key);
+
TempData["Message"] = String.Format(
CultureInfo.CurrentCulture, Strings.SuccessfullyUploadedPackage, package.PackageRegistration.Id, package.Version);
@@ -643,4 +740,4 @@ private static string GetSortExpression(string sortOrder)
}
}
}
-}
+}
View
5 Website/Controllers/PagesController.cs
@@ -10,6 +10,11 @@ public PagesController()
{
}
+ public virtual ActionResult Contact()
+ {
+ return View();
+ }
+
public virtual ActionResult Home()
{
return View();
View
23 Website/Controllers/StatisticsController.cs
@@ -141,11 +141,30 @@ private CultureInfo DetermineClientLocale()
return new HttpStatusCodeResult(HttpStatusCode.NotFound);
}
- bool isAvailable = await _statisticsService.LoadPackageDownloadsByVersion(id);
+ StatisticsPackagesReport report = await _statisticsService.GetPackageDownloadsByVersion(id);
var model = new StatisticsPackagesViewModel();
- model.SetPackageDownloadsByVersion(id, isAvailable, _statisticsService.PackageDownloadsByVersion);
+ model.SetPackageDownloadsByVersion(id, report);
+
+ return View(model);
+ }
+
+ //
+ // GET: /stats/package/{id}/{version}
+
+ public virtual async Task<ActionResult> PackageDownloadsDetail(string id, string version)
+ {
+ if (_statisticsService == null)
+ {
+ return new HttpStatusCodeResult(HttpStatusCode.NotFound);
+ }
+
+ StatisticsPackagesReport report = await _statisticsService.GetPackageVersionDownloadsByClient(id, version);
+
+ var model = new StatisticsPackagesViewModel();
+
+ model.SetPackageVersionDownloadsByClient(id, version, report);
return View(model);
}
View
126 Website/DataServices/FeedServiceBase.cs
@@ -2,26 +2,16 @@
using System.Data.Services;
using System.Data.Services.Common;
using System.Data.Services.Providers;
-using System.Diagnostics;
using System.IO;
-using System.Linq;
using System.ServiceModel;
using System.Web;
using System.Web.Mvc;
-using Microsoft.Data.OData.Query;
-using Microsoft.Data.OData.Query.SyntacticAst;
-using QueryInterceptor;
namespace NuGetGallery
{
[ServiceBehavior(IncludeExceptionDetailInFaults = true, ConcurrencyMode = ConcurrencyMode.Multiple)]
public abstract class FeedServiceBase<TPackage> : DataService<FeedContext<TPackage>>, IDataServiceStreamProvider, IServiceProvider
{
- /// <summary>
- /// Determines the maximum number of packages returned in a single page of an OData result.
- /// </summary>
- private const int MaxPageSize = 40;
-
private readonly IConfiguration _configuration;
private readonly IEntitiesContext _entities;
@@ -157,125 +147,11 @@ protected static void InitializeServiceBase(DataServiceConfiguration config)
config.SetServiceOperationAccessRule("Search", ServiceOperationRights.AllRead);
config.SetServiceOperationAccessRule("FindPackagesById", ServiceOperationRights.AllRead);
config.SetEntitySetAccessRule("Packages", EntitySetRights.AllRead);
- config.SetEntitySetPageSize("Packages", MaxPageSize);
+ config.SetEntitySetPageSize("Packages", SearchAdaptor.MaxPageSize);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
config.UseVerboseErrors = true;
}
- protected virtual IQueryable<Package> SearchCore(
- IQueryable<Package> packages, string searchTerm, string targetFramework, bool includePrerelease)
- {
- SearchFilter searchFilter;
- // We can only use Lucene if the client queries for the latest versions (IsLatest \ IsLatestStable) versions of a package
- // and specific sort orders that we have in the index.
- if (TryReadSearchFilter(HttpContext.Request, out searchFilter))
- {
- searchFilter.SearchTerm = searchTerm;
- searchFilter.IncludePrerelease = includePrerelease;
-
- return GetResultsFromSearchService(packages, searchFilter);
- }
-
- if (!includePrerelease)
- {
- packages = packages.Where(p => !p.IsPrerelease);
- }
- return packages.Search(searchTerm);
- }
-
- private IQueryable<Package> GetResultsFromSearchService(IQueryable<Package> packages, SearchFilter searchFilter)
- {
- int totalHits;
- var result = SearchService.Search(packages, searchFilter, out totalHits);
-
- // For count queries, we can ask the SearchService to not filter the source results. This would avoid hitting the database and consequently make
- // it very fast.
- if (searchFilter.CountOnly)
- {
- // At this point, we already know what the total count is. We can have it return this value very quickly without doing any SQL.
- return result.InterceptWith(new CountInterceptor(totalHits));
- }
-
- // For relevance search, Lucene returns us a paged\sorted list. OData tries to apply default ordering and Take \ Skip on top of this.
- // We avoid it by yanking these expressions out of out the tree.
- return result.InterceptWith(new DisregardODataInterceptor());
- }
-
- private bool TryReadSearchFilter(HttpRequestBase request, out SearchFilter searchFilter)
- {
- var odataQuery = SyntacticTree.ParseUri(new Uri(SiteRoot + request.RawUrl), new Uri(SiteRoot));
-
- var keywordPath = odataQuery.Path as KeywordSegmentQueryToken;
- searchFilter = new SearchFilter
- {
- // HACK: The way the default paging works is WCF attempts to read up to the MaxPageSize elements. If it finds as many, it'll assume there
- // are more elements to be paged and generate a continuation link. Consequently we'll always ask to pull MaxPageSize elements so WCF generates the
- // link for us and then allow it to do a Take on the results. The alternative to do is roll our IDataServicePagingProvider, but we run into
- // issues since we need to manage state over concurrent requests. This seems like an easier solution.
- Take = MaxPageSize,
- Skip = odataQuery.Skip ?? 0,
- CountOnly = keywordPath != null && keywordPath.Keyword == KeywordKind.Count,
- SortDirection = SortDirection.Ascending
- };
-
- var filterProperty = odataQuery.Filter as PropertyAccessQueryToken;
- if (filterProperty == null ||
- !(filterProperty.Name.Equals("IsLatestVersion", StringComparison.Ordinal) ||
- filterProperty.Name.Equals("IsAbsoluteLatestVersion", StringComparison.Ordinal)))
- {
- // We'll only use the index if we the query searches for latest \ latest-stable packages
- return false;
- }
-
- var orderBy = odataQuery.OrderByTokens.FirstOrDefault();
- if (orderBy == null || orderBy.Expression == null)
- {
- searchFilter.SortProperty = SortProperty.Relevance;
- }
- else if (orderBy.Expression.Kind == QueryTokenKind.PropertyAccess)
- {
- var propertyAccess = (PropertyAccessQueryToken)orderBy.Expression;
- if (propertyAccess.Name.Equals("DownloadCount", StringComparison.Ordinal))
- {
- searchFilter.SortProperty = SortProperty.DownloadCount;
- }
- else if (propertyAccess.Name.Equals("Published", StringComparison.Ordinal))
- {
- searchFilter.SortProperty = SortProperty.Recent;
- }
- else if (propertyAccess.Name.Equals("Id", StringComparison.Ordinal))
- {
- searchFilter.SortProperty = SortProperty.DisplayName;
- }
- else
- {
- Debug.WriteLine("Order by clause {0} is unsupported", propertyAccess.Name);
- return false;
- }
- }
- else if (orderBy.Expression.Kind == QueryTokenKind.FunctionCall)
- {
- var functionCall = (FunctionCallQueryToken)orderBy.Expression;
- if (functionCall.Name.Equals("concat", StringComparison.OrdinalIgnoreCase))
- {
- // We'll assume this is concat(Title, Id)
- searchFilter.SortProperty = SortProperty.DisplayName;
- searchFilter.SortDirection = orderBy.Direction == OrderByDirection.Descending ? SortDirection.Descending : SortDirection.Ascending;
- }
- else
- {
- Debug.WriteLine("Order by clause {0} is unsupported", functionCall.Name);
- return false;
- }
- }
- else
- {
- Debug.WriteLine("Order by clause {0} is unsupported", orderBy.Expression.Kind);
- return false;
- }
- return true;
- }
-
protected virtual bool UseHttps()
{
return HttpContext.Request.IsSecureConnection;
View
2  Website/DataServices/PackageExtensions.cs
@@ -27,7 +27,7 @@ public static IQueryable<V1FeedPackage> ToV1FeedPackageQuery(this IQueryable<Pac
Dependencies = p.FlattenedDependencies,
Description = p.Description,
DownloadCount = p.PackageRegistration.DownloadCount,
- ExternalPackageUrl = p.ExternalPackageUrl,
+ ExternalPackageUrl = null,
GalleryDetailsUrl = siteRoot + "packages/" + p.PackageRegistration.Id + "/" + p.Version,
IconUrl = p.IconUrl,
IsLatestVersion = p.IsLatestStable,
View
144 Website/DataServices/SearchAdaptor.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Web;
+using Microsoft.Data.OData.Query;
+using Microsoft.Data.OData.Query.SyntacticAst;
+using QueryInterceptor;
+
+namespace NuGetGallery
+{
+ public static class SearchAdaptor
+ {
+ /// <summary>
+ /// Determines the maximum number of packages returned in a single page of an OData result.
+ /// </summary>
+ internal const int MaxPageSize = 40;
+
+ public static IQueryable<Package> SearchCore(
+ ISearchService searchService,
+ HttpRequestBase request,
+ string siteRoot,
+ IQueryable<Package> packages,
+ string searchTerm,
+ string targetFramework,
+ bool includePrerelease,
+ IQueryable<Package> filterToPackageSet = null)
+ {
+ SearchFilter searchFilter;
+ // We can only use Lucene if the client queries for the latest versions (IsLatest \ IsLatestStable) versions of a package
+ // and specific sort orders that we have in the index.
+ if (TryReadSearchFilter(request, siteRoot, out searchFilter))
+ {
+ searchFilter.SearchTerm = searchTerm;
+ searchFilter.IncludePrerelease = includePrerelease;
+
+ Trace.WriteLine("TODO: use target framework parameter - see #856" + targetFramework);
+
+ var results = GetResultsFromSearchService(searchService, searchFilter, filterToPackageSet);
+
+ return results;
+ }
+
+ if (!includePrerelease)
+ {
+ packages = packages.Where(p => !p.IsPrerelease);
+ }
+ return packages.Search(searchTerm);
+ }
+
+ private static bool TryReadSearchFilter(HttpRequestBase request, string siteRoot, out SearchFilter searchFilter)
+ {
+ var odataQuery = SyntacticTree.ParseUri(new Uri(siteRoot + request.RawUrl), new Uri(siteRoot));
+
+ var keywordPath = odataQuery.Path as KeywordSegmentQueryToken;
+ searchFilter = new SearchFilter
+ {
+ // HACK: The way the default paging works is WCF attempts to read up to the MaxPageSize elements. If it finds as many, it'll assume there
+ // are more elements to be paged and generate a continuation link. Consequently we'll always ask to pull MaxPageSize elements so WCF generates the
+ // link for us and then allow it to do a Take on the results. The alternative to do is roll our IDataServicePagingProvider, but we run into
+ // issues since we need to manage state over concurrent requests. This seems like an easier solution.
+ Take = MaxPageSize,
+ Skip = odataQuery.Skip ?? 0,
+ CountOnly = keywordPath != null && keywordPath.Keyword == KeywordKind.Count,
+ SortDirection = SortDirection.Ascending
+ };
+
+ var filterProperty = odataQuery.Filter as PropertyAccessQueryToken;
+ if (filterProperty == null ||
+ !(filterProperty.Name.Equals("IsLatestVersion", StringComparison.Ordinal) ||
+ filterProperty.Name.Equals("IsAbsoluteLatestVersion", StringComparison.Ordinal)))
+ {
+ // We'll only use the index if we the query searches for latest \ latest-stable packages
+ return false;
+ }
+
+ var orderBy = odataQuery.OrderByTokens.FirstOrDefault();
+ if (orderBy == null || orderBy.Expression == null)
+ {
+ searchFilter.SortProperty = SortProperty.Relevance;
+ }
+ else if (orderBy.Expression.Kind == QueryTokenKind.PropertyAccess)
+ {
+ var propertyAccess = (PropertyAccessQueryToken)orderBy.Expression;
+ if (propertyAccess.Name.Equals("DownloadCount", StringComparison.Ordinal))
+ {
+ searchFilter.SortProperty = SortProperty.DownloadCount;
+ }
+ else if (propertyAccess.Name.Equals("Published", StringComparison.Ordinal))
+ {
+ searchFilter.SortProperty = SortProperty.Recent;
+ }
+ else if (propertyAccess.Name.Equals("Id", StringComparison.Ordinal))
+ {
+ searchFilter.SortProperty = SortProperty.DisplayName;
+ }
+ else
+ {
+ Debug.WriteLine("Order by clause {0} is unsupported", propertyAccess.Name);
+ return false;
+ }
+ }
+ else if (orderBy.Expression.Kind == QueryTokenKind.FunctionCall)
+ {
+ var functionCall = (FunctionCallQueryToken)orderBy.Expression;
+ if (functionCall.Name.Equals("concat", StringComparison.OrdinalIgnoreCase))
+ {
+ // We'll assume this is concat(Title, Id)
+ searchFilter.SortProperty = SortProperty.DisplayName;
+ searchFilter.SortDirection = orderBy.Direction == OrderByDirection.Descending ? SortDirection.Descending : SortDirection.Ascending;
+ }
+ else
+ {
+ Debug.WriteLine("Order by clause {0} is unsupported", functionCall.Name);
+ return false;
+ }
+ }
+ else
+ {
+ Debug.WriteLine("Order by clause {0} is unsupported", orderBy.Expression.Kind);
+ return false;
+ }
+ return true;
+ }
+
+ private static IQueryable<Package> GetResultsFromSearchService(ISearchService searchService, SearchFilter searchFilter, IQueryable<Package> filterToPackageSet)
+ {
+ int totalHits;
+ var result = searchService.Search(searchFilter, out totalHits, filterToPackageSet);
+
+ // For count queries, we can ask the SearchService to not filter the source results. This would avoid hitting the database and consequently make
+ // it very fast.
+ if (searchFilter.CountOnly)
+ {
+ // At this point, we already know what the total count is. We can have it return this value very quickly without doing any SQL.
+ return result.InterceptWith(new CountInterceptor(totalHits));
+ }
+
+ // For relevance search, Lucene returns us a paged\sorted list. OData tries to apply default ordering and Take \ Skip on top of this.
+ // We avoid it by yanking these expressions out of out the tree.
+ return result.InterceptWith(new DisregardODataInterceptor());
+ }
+ }
+}
View
2  Website/DataServices/V1Feed.svc.cs
@@ -66,7 +66,7 @@ public IQueryable<V1FeedPackage> Search(string searchTerm, string targetFramewor
.Where(p => p.Listed && !p.IsPrerelease);
// For v1 feed, only allow stable package versions.
- packages = SearchCore(packages, searchTerm, targetFramework, includePrerelease: false);
+ packages = SearchAdaptor.SearchCore(SearchService, HttpContext.Request, SiteRoot, packages, searchTerm, targetFramework, includePrerelease: false);
return packages.ToV1FeedPackageQuery(Configuration.GetSiteRoot(UseHttps()));
}
}
View
2  Website/DataServices/V1FeedPackage.cs
@@ -20,7 +20,7 @@ public class V1FeedPackage
public string Dependencies { get; set; }
public string Description { get; set; }
public int DownloadCount { get; set; }
- public string ExternalPackageUrl { get; set; }
+ public string ExternalPackageUrl { get; set; } // deprecated: always null/empty
public string GalleryDetailsUrl { get; set; }
public string IconUrl { get; set; }
public bool IsLatestVersion { get; set; }
View
11 Website/DataServices/V2CuratedFeed.svc.cs
@@ -118,14 +118,9 @@ protected override void OnStartProcessingRequest(ProcessRequestArgs args)
[WebGet]
public IQueryable<V2FeedPackage> Search(string searchTerm, string targetFramework, bool includePrerelease)
{
- var packages = GetPackages();
-
- packages = packages.Where(p => p.Listed);
- if (!includePrerelease)
- {
- packages = packages.Where(p => !p.IsPrerelease);
- }
- return packages.Search(searchTerm).ToV2FeedPackageQuery(Configuration.GetSiteRoot(UseHttps()));
+ IQueryable<Package> curatedPackages = GetPackages();
+ return SearchAdaptor.SearchCore(SearchService, HttpContext.Request, SiteRoot, curatedPackages, searchTerm, targetFramework, includePrerelease, filterToPackageSet: curatedPackages)
+ .ToV2FeedPackageQuery(Configuration.GetSiteRoot(UseHttps()));
}
public override Uri GetReadStreamUri(
View
2  Website/DataServices/V2Feed.svc.cs
@@ -48,7 +48,7 @@ public IQueryable<V2FeedPackage> Search(string searchTerm, string targetFramewor
.Include(p => p.PackageRegistration)
.Include(p => p.PackageRegistration.Owners)
.Where(p => p.Listed);
- return SearchCore(packages, searchTerm, targetFramework, includePrerelease).ToV2FeedPackageQuery(GetSiteRoot());
+ return SearchAdaptor.SearchCore(SearchService, HttpContext.Request, SiteRoot, packages, searchTerm, targetFramework, includePrerelease).ToV2FeedPackageQuery(GetSiteRoot());
}
[WebGet]
View
4 Website/Errors/Error.html
@@ -43,6 +43,10 @@
<footer id="footer">
<ul class="recommended">
<li>
+ <a href="@Url.Action(MVC.Pages.Contact())">Contact Us</a>
+ <p>Got questions about NuGet or the NuGet Gallery?</p>
+ </li>
+ <li>
<a href="http://docs.nuget.org/docs/start-here/overview">Overview</a>
<p>NuGet is a Visual Studio 2010 extension that makes it easy to add, remove, and update libraries and...</p>
</li>
View
4 Website/Errors/ErrorLayout.cshtml
@@ -34,6 +34,10 @@
<footer id="footer">
<ul class="recommended">
<li>
+ <a href="@Url.Action(MVC.Pages.Contact())">Contact Us</a>
+ <p>Got questions about NuGet or the NuGet Gallery?</p>
+ </li>
+ <li>
<a href="http://docs.nuget.org/docs/start-here/overview">Overview</a>
<p>NuGet is a Visual Studio 2010 extension that makes it easy to add, remove, and update libraries and...</p>
</li>
View
65 Website/Helpers/PackageHelper.cs
@@ -1,65 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Net;
-using System.Threading.Tasks;
-using NuGet;
-
-using HttpClient = System.Net.Http.HttpClient;
-
-namespace NuGetGallery.Helpers
-{
- internal class PackageHelper
- {
- /// <summary>
- /// Look for the INupkg instance in the cache first. If it's in the cache, return it.
- /// Otherwise, download the package from the storage service and store it into the cache.
- /// </summary>
- public static async Task<INupkg> GetPackageFromCacheOrDownloadIt(
- Package package,
- IPackageCacheService cacheService,
- IPackageFileService packageFileService)
- {
- Debug.Assert(package != null);
- Debug.Assert(cacheService != null);
- Debug.Assert(packageFileService != null);
-
- string cacheKey = CreateCacheKey(package.PackageRegistration.Id, package.Version);
- byte[] buffer = cacheService.GetBytes(cacheKey);
- if (buffer == null)
- {
- // In the past, some very old packages can specify an external package binary not hosted at nuget.org.
- // We no longer allow that today.
- if (!String.IsNullOrEmpty(package.ExternalPackageUrl))
- {
- var httpClient = new HttpClient();
- using (var responseStream = await httpClient.GetStreamAsync(package.ExternalPackageUrl))
- {
- buffer = responseStream.ReadAllBytes();
- }
- }
- else
- {
- using (Stream stream = await packageFileService.DownloadPackageFileAsync(package))
- {
- if (stream == null)
- {
- throw new InvalidOperationException("Couldn't download the package from the storage.");
- }
-
- buffer = stream.ReadAllBytes();
- }
- }
-
- cacheService.SetBytes(cacheKey, buffer);
- }
-
- return new Nupkg(new MemoryStream(buffer), leaveOpen: false);
- }
-
- private static string CreateCacheKey(string id, string version)
- {
- return id.ToLowerInvariant() + "." + version.ToLowerInvariant();
- }
- }
-}
View
53 Website/Infrastructure/Lucene/IntersectionFilter.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using Lucene.Net.Index;
+using Lucene.Net.Search;
+using Lucene.Net.Util;
+
+namespace NuGetGallery.Infrastructure.Lucene
+{
+ /// <summary>
+ /// Filter that returns only items in the intersection of the two input filters.
+ /// </summary>
+ public class IntersectionFilter : Filter
+ {
+ private Filter _filter2;
+ private Filter _filter1;
+
+ public IntersectionFilter(Filter filter1, Filter filter2)
+ {
+ _filter1 = filter1;
+ _filter2 = filter2;
+ }
+
+ public override DocIdSet GetDocIdSet(IndexReader reader)
+ {
+ var set1 = _filter1.GetDocIdSet(reader).Iterator();
+ var set2 = _filter2.GetDocIdSet(reader).Iterator();
+
+ var intersection = new List<int>();
+ int head1 = set1.NextDoc();
+ int head2 = set2.Advance(head1);
+ while (head1 != DocIdSetIterator.NO_MORE_DOCS && head2 != DocIdSetIterator.NO_MORE_DOCS)
+ {
+ if (head1 == head2)
+ {
+ intersection.Add(head1);
+ head1 = set1.NextDoc();
+ head2 = set2.Advance(head1);
+ }
+ else if (head1 < head2)
+ {
+ head1 = set1.Advance(head2);
+ }
+ else
+ {
+ Debug.Assert(head1 > head2);
+ head2 = set2.Advance(head1);
+ }
+ }
+
+ return new SortedVIntList(intersection.ToArray());
+ }
+ }
+}
View
2  Website/Infrastructure/Lucene/LuceneIndexingService.cs
@@ -182,7 +182,6 @@ private void AddPackage(Package package)
document.Add(new Field("Language", package.Language.ToStringSafe(), Field.Store.YES, Field.Index.NO));
document.Add(new Field("LicenseUrl", package.LicenseUrl.ToStringSafe(), Field.Store.YES, Field.Index.NO));
document.Add(new Field("MinClientVersion", package.MinClientVersion.ToStringSafe(), Field.Store.YES, Field.Index.NO));
- document.Add(new Field("Key", package.Key.ToString(CultureInfo.InvariantCulture), Field.Store.YES, Field.Index.NO));
document.Add(new Field("Version", package.Version.ToStringSafe(), Field.Store.YES, Field.Index.NO));
document.Add(new Field("VersionDownloadCount", package.DownloadCount.ToString(CultureInfo.InvariantCulture), Field.Store.YES, Field.Index.NO));
document.Add(new Field("PackageFileSize", package.PackageFileSize.ToString(CultureInfo.InvariantCulture), Field.Store.YES, Field.Index.NO));
@@ -201,6 +200,7 @@ private void AddPackage(Package package)
// Fields meant for filtering, also storing data to avoid hitting SQL while doing searches
document.Add(new Field("IsLatest", package.IsLatest.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
document.Add(new Field("IsLatestStable", package.IsLatestStable.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
+ document.Add(new Field("Key", package.Key.ToString(CultureInfo.InvariantCulture), Field.Store.YES, Field.Index.NOT_ANALYZED));
// Note: Used to identify index records for updates
document.Add(new Field("PackageRegistrationKey",
View
19 Website/Infrastructure/Lucene/LuceneSearchService.cs
@@ -8,6 +8,7 @@
using System.Linq;
using System.Globalization;
using NuGetGallery.Helpers;
+using NuGetGallery.Infrastructure.Lucene;
namespace NuGetGallery
{
@@ -23,13 +24,8 @@ public LuceneSearchService(Lucene.Net.Store.Directory directory)
_directory = directory;
}
- public IQueryable<Package> Search(IQueryable<Package> packages, SearchFilter searchFilter, out int totalHits)
+ public IQueryable<Package> Search(SearchFilter searchFilter, out int totalHits, IQueryable<Package> filterToPackageSet = null)
{
- if (packages == null)
- {
- throw new ArgumentNullException("packages");
- }
-
if (searchFilter == null)
{
throw new ArgumentNullException("searchFilter");
@@ -45,10 +41,10 @@ public IQueryable<Package> Search(IQueryable<Package> packages, SearchFilter sea
throw new ArgumentOutOfRangeException("searchFilter");
}
- return SearchCore(searchFilter, out totalHits);
+ return SearchCore(searchFilter, out totalHits, filterToPackageSet);
}
- private IQueryable<Package> SearchCore(SearchFilter searchFilter, out int totalHits)
+ private IQueryable<Package> SearchCore(SearchFilter searchFilter, out int totalHits, IQueryable<Package> filterToPackageSet)
{
int numRecords = searchFilter.Skip + searchFilter.Take;
@@ -64,7 +60,12 @@ private IQueryable<Package> SearchCore(SearchFilter searchFilter, out int totalH
var filterTerm = searchFilter.IncludePrerelease ? "IsLatest" : "IsLatestStable";
var termQuery = new TermQuery(new Term(filterTerm, Boolean.TrueString));
- var filter = new QueryWrapperFilter(termQuery);
+ Filter filter = new QueryWrapperFilter(termQuery);
+ if (filterToPackageSet != null)
+ {
+ filter = new IntersectionFilter(new PackageSetFilter(filterToPackageSet), filter);
+ }
+
var results = searcher.Search(query, filter: filter, n: numRecords, sort: new Sort(GetSortField(searchFilter)));
totalHits = results.totalHits;
View
57 Website/Infrastructure/Lucene/PackageSetFilter.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using Lucene.Net.Index;
+using Lucene.Net.Search;
+using Lucene.Net.Util;
+
+namespace NuGetGallery.Infrastructure.Lucene
+{
+ /// <summary>
+ /// This filter contains a set of acceptable queryable package IDs,
+ /// it will look up all of the documents in the Lucene Index based on that complete set of package IDs
+ /// and output a DocIdSet used to filter Lucene search results, restricting it to the input set.
+ /// </summary>
+ internal class PackageSetFilter : Filter
+ {
+ private readonly int[] _keys;
+
+ // filterTo needs to be passed as IQueryable for decent perf.
+ public PackageSetFilter(IQueryable<Package> filterTo)
+ {
+ if (filterTo == null)
+ {
+ throw new ArgumentNullException("filterTo");
+ }
+
+ _keys = filterTo.Select(p => p.Key).ToArray();
+ }
+
+ public override DocIdSet GetDocIdSet(IndexReader reader)
+ {
+ var docIds = new int[_keys.Length];
+
+ for (int i = 0; i < _keys.Length; i++)
+ {
+ var docs = reader.TermDocs(new Term("Key", _keys[i].ToString(CultureInfo.InvariantCulture)));
+ if (docs.Next())
+ {
+ docIds[i] = docs.Doc();
+ }
+ else
+ {
+ // We should nearly always be able to find a curated feed package in the lucene index, but it's possible the index is not up to date in which case we miss
+ docIds[i] = -1;
+ }
+
+ Debug.Assert(!docs.Next(), "There should not be multiple matching documents with the same package 'Key' in the index.");
+ }
+
+ var goodDocIds = docIds.Where(id => id != -1).ToArray();
+ Array.Sort(goodDocIds);
+ return new SortedVIntList(goodDocIds);
+ }
+ }
+}
View
22 Website/PackagesController.generated.cs
@@ -48,6 +48,11 @@ public partial class PackagesController {
}
[NonAction]
[GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode]
+ public System.Web.Mvc.ActionResult ReportMyPackage() {
+ return new T4MVC_ActionResult(Area, Name, ActionNames.ReportMyPackage);
+ }
+ [NonAction]
+ [GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode]
public System.Web.Mvc.ActionResult ContactOwners() {
return new T4MVC_ActionResult(Area, Name, ActionNames.ContactOwners);
}
@@ -88,6 +93,7 @@ public class ActionNamesClass {
public readonly string DisplayPackage = "DisplayPackage";
public readonly string ListPackages = "ListPackages";
public readonly string ReportAbuse = "ReportAbuse";
+ public readonly string ReportMyPackage = "ReportMyPackage";
public readonly string ContactOwners = "ContactOwners";
public readonly string Download = "Download";
public readonly string ManagePackageOwners = "ManagePackageOwners";
@@ -113,6 +119,7 @@ public class ViewNames {
public readonly string ListPackages = "~/Views/Packages/ListPackages.cshtml";
public readonly string ManagePackageOwners = "~/Views/Packages/ManagePackageOwners.cshtml";
public readonly string ReportAbuse = "~/Views/Packages/ReportAbuse.cshtml";
+ public readonly string ReportMyPackage = "~/Views/Packages/ReportMyPackage.cshtml";
public readonly string UnverifiablePackage = "~/Views/Packages/UnverifiablePackage.cshtml";
public readonly string UploadPackage = "~/Views/Packages/UploadPackage.cshtml";
public readonly string VerifyPackage = "~/Views/Packages/VerifyPackage.cshtml";
@@ -151,6 +158,13 @@ public class T4MVC_PackagesController: NuGetGallery.PackagesController {
return callInfo;
}
+ public override System.Web.Mvc.ActionResult ReportMyPackage(string id, string version) {
+ var callInfo = new T4MVC_ActionResult(Area, Name, ActionNames.ReportMyPackage);
+ callInfo.RouteValueDictionary.Add("id", id);
+ callInfo.RouteValueDictionary.Add("version", version);
+ return callInfo;
+ }
+
public override System.Web.Mvc.ActionResult ReportAbuse(string id, string version, NuGetGallery.ReportAbuseViewModel reportForm) {
var callInfo = new T4MVC_ActionResult(Area, Name, ActionNames.ReportAbuse);
callInfo.RouteValueDictionary.Add("id", id);
@@ -159,6 +173,14 @@ public class T4MVC_PackagesController: NuGetGallery.PackagesController {
return callInfo;
}
+ public override System.Web.Mvc.ActionResult ReportMyPackage(string id, string version, NuGetGallery.ReportAbuseViewModel reportForm) {
+ var callInfo = new T4MVC_ActionResult(Area, Name, ActionNames.ReportMyPackage);
+ callInfo.RouteValueDictionary.Add("id", id);
+ callInfo.RouteValueDictionary.Add("version", version);
+ callInfo.RouteValueDictionary.Add("reportForm", reportForm);
+ return callInfo;
+ }
+
public override System.Web.Mvc.ActionResult ContactOwners(string id) {
var callInfo = new T4MVC_ActionResult(Area, Name, ActionNames.ContactOwners);
callInfo.RouteValueDictionary.Add("id", id);
View
7 Website/PagesController.generated.cs
@@ -44,6 +44,7 @@ public partial class PagesController {
public ActionNamesClass ActionNames { get { return s_actions; } }
[GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode]
public class ActionNamesClass {
+ public readonly string Contact = "Contact";
public readonly string Home = "Home";
public readonly string Terms = "Terms";
public readonly string Privacy = "Privacy";
@@ -55,6 +56,7 @@ public class ActionNamesClass {
public ViewNames Views { get { return s_views; } }
[GeneratedCode("T4MVC", "2.0"), DebuggerNonUserCode]
public class ViewNames {
+ public readonly string Contact = "~/Views/Pages/Contact.cshtml";
public readonly string Home = "~/Views/Pages/Home.cshtml";
public readonly string Privacy = "~/Views/Pages/Privacy.cshtml";
public readonly string Terms = "~/Views/Pages/Terms.cshtml";
@@ -65,6 +67,11 @@ public class ViewNames {
public class T4MVC_PagesController: NuGetGallery.PagesController {
public T4MVC_PagesController() : base(Dummy.Instance) { }
+ public override System.Web.Mvc.ActionResult Contact() {
+ var callInfo = new T4MVC_ActionResult(Area, Name, ActionNames.Contact);
+ return callInfo;
+ }
+
public override System.Web.Mvc.ActionResult Home() {
var callInfo = new T4MVC_ActionResult(Area, Name, ActionNames.Home);
return callInfo;
View
1  Website/RouteNames.cs
@@ -41,5 +41,6 @@ public static class RouteName
public const string StatisticsPackages = "StatisticsPackages";
public const string StatisticsPackageVersions = "StatisticsPackageVersions";
public const string StatisticsPackageDownloadsByVersion = "StatisticsPackageDownloadsByVersion";
+ public const string StatisticsPackageDownloadsDetail = "StatisticsPackageDownloadsDetail";
}
}
View
3,583 Website/Scripts/knockout-2.2.1.debug.js
3,583 additions, 0 deletions not shown
View
85 Website/Scripts/knockout-2.2.1.js