diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ImageTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ImageTagHelper.cs new file mode 100644 index 0000000000..7cb42ddf71 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ImageTagHelper.cs @@ -0,0 +1,84 @@ +// 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 Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Mvc.TagHelpers.Internal; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.Framework.Caching.Memory; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + /// + /// implementation targeting <img> elements that supports file versioning. + /// + /// + /// The tag helper won't process for cases with just the 'src' attribute. + /// + [TargetElement("img", Attributes = FileVersionAttributeName + "," + SrcAttributeName)] + public class ImageTagHelper : TagHelper + { + private static readonly string Namespace = typeof(ImageTagHelper).Namespace; + + private const string FileVersionAttributeName = "asp-file-version"; + private const string SrcAttributeName = "src"; + + private FileVersionProvider _fileVersionProvider; + + /// + /// Source of the image. + /// + /// + /// Passed through to the generated HTML in all cases. + /// + [HtmlAttributeName(SrcAttributeName)] + public string Src { get; set; } + + /// + /// Value indicating if file version should be appended to the src urls. + /// + /// + /// If true then a query string "v" with the encoded content of the file is added. + /// + [HtmlAttributeName(FileVersionAttributeName)] + public bool FileVersion { get; set; } + + [Activate] + [HtmlAttributeNotBound] + public IHostingEnvironment HostingEnvironment { get; set; } + + [Activate] + [HtmlAttributeNotBound] + public ViewContext ViewContext { get; set; } + + [Activate] + [HtmlAttributeNotBound] + public IMemoryCache Cache { get; set; } + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (FileVersion) + { + EnsureFileVersionProvider(); + output.Attributes[SrcAttributeName] = _fileVersionProvider.AddFileVersionToPath(Src); + } + else + { + // Pass through attribute that is also a well-known HTML attribute. + output.CopyHtmlAttribute(SrcAttributeName, context); + } + } + + private void EnsureFileVersionProvider() + { + if (_fileVersionProvider == null) + { + _fileVersionProvider = new FileVersionProvider( + HostingEnvironment.WebRootFileProvider, + Cache, + ViewContext.HttpContext.Request.PathBase); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs index a234dda491..d07f243643 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTest.cs @@ -47,6 +47,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests [InlineData("Link", null)] // Testing the ScriptTagHelper [InlineData("Script", null)] + // Testing the ImageTagHelper + [InlineData("Image", null)] // Testing InputTagHelper with File [InlineData("Input", null)] public async Task MvcTagHelpers_GeneratesExpectedResults(string action, string antiForgeryPath) diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Image.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Image.html new file mode 100644 index 0000000000..1eb2c29e9c --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/compiler/resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Image.html @@ -0,0 +1,21 @@ + + + + + + Image + + + + +

Image Tag Helper Test

+ + Red block + + + Red versioned + + + Red explicitly not versioned + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ImageTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ImageTagHelperTest.cs new file mode 100644 index 0000000000..e3298d2854 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ImageTagHelperTest.cs @@ -0,0 +1,288 @@ +// 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.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.FileProviders; +using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.Caching; +using Microsoft.Framework.Caching.Memory; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + public class ImageTagHelperTest + { + + [Fact] + public void PreservesOrderOfSourceAttributesWhenRun() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + { "alt", new HtmlString("alt text") }, + { "data-extra", new HtmlString("something") }, + { "title", new HtmlString("Image title") }, + { "src", "testimage.png" }, + { "asp-file-version", "true" } + }); + var output = MakeImageTagHelperOutput( + attributes: new TagHelperAttributeList + { + { "alt", new HtmlString("alt text") }, + { "data-extra", new HtmlString("something") }, + { "title", new HtmlString("Image title") }, + }); + + var expectedOutput = MakeImageTagHelperOutput( + attributes: new TagHelperAttributeList + { + { "alt", new HtmlString("alt text") }, + { "data-extra", new HtmlString("something") }, + { "title", new HtmlString("Image title") }, + { "src", "testimage.png?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk" } + }); + + var hostingEnvironment = MakeHostingEnvironment(); + var viewContext = MakeViewContext(); + var helper = new ImageTagHelper + { + HostingEnvironment = hostingEnvironment, + ViewContext = viewContext, + Src = "testimage.png", + FileVersion = true, + Cache = MakeCache(), + }; + + // Act + helper.Process(context, output); + + // Assert + Assert.Equal(expectedOutput.TagName, output.TagName); + Assert.Equal(4, output.Attributes.Count); + + for(int i=0; i < expectedOutput.Attributes.Count; i++) + { + var expectedAtribute = expectedOutput.Attributes[i]; + var actualAttribute = output.Attributes[i]; + Assert.Equal(expectedAtribute.Name, actualAttribute.Name); + Assert.Equal(expectedAtribute.Value.ToString(), actualAttribute.Value.ToString()); + } + } + + [Fact] + public void RendersImageTag_AddsFileVersion() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + { "alt", new HtmlString("Alt image text") }, + { "src", "/images/test-image.png" }, + { "asp-file-version", "true" } + }); + var output = MakeImageTagHelperOutput(attributes: new TagHelperAttributeList + { + { "alt", new HtmlString("Alt image text") }, + }); + var hostingEnvironment = MakeHostingEnvironment(); + var viewContext = MakeViewContext(); + var helper = new ImageTagHelper + { + HostingEnvironment = hostingEnvironment, + ViewContext = viewContext, + Src = "/images/test-image.png", + FileVersion = true, + Cache = MakeCache(), + }; + + // Act + helper.Process(context, output); + + // Assert + Assert.True(output.Content.IsEmpty); + Assert.Equal("img", output.TagName); + Assert.Equal(2, output.Attributes.Count); + var srcAttribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("src")); + Assert.Equal("/images/test-image.png?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", srcAttribute.Value); + } + + [Fact] + public void RendersImageTag_DoesNotAddFileVersion() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + { "alt", new HtmlString("Alt image text") }, + { "src", "/images/test-image.png" }, + { "asp-file-version", "false" } + }); + var output = MakeImageTagHelperOutput(attributes: new TagHelperAttributeList + { + { "alt", new HtmlString("Alt image text") }, + }); + var hostingEnvironment = MakeHostingEnvironment(); + var viewContext = MakeViewContext(); + var helper = new ImageTagHelper + { + HostingEnvironment = hostingEnvironment, + ViewContext = viewContext, + Src = "/images/test-image.png", + FileVersion = false, + Cache = MakeCache(), + }; + + // Act + helper.Process(context, output); + + // Assert + Assert.True(output.Content.IsEmpty); + Assert.Equal("img", output.TagName); + Assert.Equal(2, output.Attributes.Count); + var srcAttribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("src")); + Assert.Equal("/images/test-image.png", srcAttribute.Value); + } + + [Fact] + public void RendersImageTag_AddsFileVersion_WithRequestPathBase() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new TagHelperAttributeList + { + { "alt", new HtmlString("alt text") }, + { "src", "/bar/images/image.jpg" }, + { "asp-file-version", "true" }, + }); + var output = MakeImageTagHelperOutput(attributes: new TagHelperAttributeList + { + { "alt", new HtmlString("alt text") }, + }); + var hostingEnvironment = MakeHostingEnvironment(); + var viewContext = MakeViewContext("/bar"); + var helper = new ImageTagHelper + { + HostingEnvironment = hostingEnvironment, + ViewContext = viewContext, + Src = "/bar/images/image.jpg", + FileVersion = true, + Cache = MakeCache(), + }; + + // Act + helper.Process(context, output); + // Assert + Assert.True(output.Content.IsEmpty); + Assert.Equal("img", output.TagName); + Assert.Equal(2, output.Attributes.Count); + var srcAttribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("src")); + Assert.Equal("/bar/images/image.jpg?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", srcAttribute.Value); + } + + private static ViewContext MakeViewContext(string requestPathBase = null) + { + var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + if (requestPathBase != null) + { + actionContext.HttpContext.Request.PathBase = new Http.PathString(requestPathBase); + } + + var metadataProvider = new EmptyModelMetadataProvider(); + var viewData = new ViewDataDictionary(metadataProvider); + var viewContext = new ViewContext( + actionContext, + Mock.Of(), + viewData, + Mock.Of(), + TextWriter.Null); + + return viewContext; + } + + private static TagHelperContext MakeTagHelperContext( + TagHelperAttributeList attributes) + { + return new TagHelperContext( + attributes, + items: new Dictionary(), + uniqueId: Guid.NewGuid().ToString("N"), + getChildContentAsync: () => + { + var tagHelperContent = new DefaultTagHelperContent(); + tagHelperContent.SetContent(default(string)); + return Task.FromResult(tagHelperContent); + }); + } + + private static TagHelperOutput MakeImageTagHelperOutput(TagHelperAttributeList attributes) + { + attributes = attributes ?? new TagHelperAttributeList(); + + return new TagHelperOutput("img", attributes); + } + + private static IHostingEnvironment MakeHostingEnvironment() + { + var emptyDirectoryContents = new Mock(); + emptyDirectoryContents.Setup(dc => dc.GetEnumerator()) + .Returns(Enumerable.Empty().GetEnumerator()); + var mockFile = new Mock(); + mockFile.SetupGet(f => f.Exists).Returns(true); + mockFile + .Setup(m => m.CreateReadStream()) + .Returns(() => new MemoryStream(Encoding.UTF8.GetBytes("Hello World!"))); + var mockFileProvider = new Mock(); + mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny())) + .Returns(emptyDirectoryContents.Object); + mockFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny())) + .Returns(mockFile.Object); + var hostingEnvironment = new Mock(); + hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object); + + return hostingEnvironment.Object; + } + + private static IMemoryCache MakeCache() + { + object result = null; + var cache = new Mock(); + cache.CallBase = true; + cache.Setup(c => c.TryGetValue(It.IsAny(), It.IsAny(), out result)) + .Returns(result != null); + + var cacheSetContext = new Mock(); + cacheSetContext.Setup(c => c.AddExpirationTrigger(It.IsAny())); + cache + .Setup( + c => c.Set( + /*key*/ It.IsAny(), + /*link*/ It.IsAny(), + /*state*/ It.IsAny(), + /*create*/ It.IsAny>())) + .Returns(( + string input, + IEntryLink entryLink, + object state, + Func create) => + { + { + cacheSetContext.Setup(c => c.State).Returns(state); + return create(cacheSetContext.Object); + } + }); + return cache.Object; + } + } +} \ No newline at end of file diff --git a/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs b/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs index cdc9d87d61..2ee3dcebc3 100644 --- a/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs +++ b/test/WebSites/MvcTagHelpersWebSite/Controllers/MvcTagHelper_HomeController.cs @@ -162,6 +162,11 @@ namespace MvcTagHelpersWebSite.Controllers return View(); } + public IActionResult Image() + { + return View(); + } + public IActionResult Script() { return View(); diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Image.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Image.cshtml new file mode 100644 index 0000000000..8874508a3a --- /dev/null +++ b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Image.cshtml @@ -0,0 +1,22 @@ +@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" + + + + + + Image + + + + +

Image Tag Helper Test

+ + Red block + + + Red versioned + + + Red explicitly not versioned + + \ No newline at end of file diff --git a/test/WebSites/MvcTagHelpersWebSite/wwwroot/images/red.png b/test/WebSites/MvcTagHelpersWebSite/wwwroot/images/red.png new file mode 100644 index 0000000000..b72127b403 Binary files /dev/null and b/test/WebSites/MvcTagHelpersWebSite/wwwroot/images/red.png differ