├── docs ├── images │ ├── dashboard.png │ ├── storage-keys.png │ ├── upload-assets.png │ ├── creating-cdn-profile.png │ ├── image-type-cropper.png │ ├── storage-containers.png │ ├── creating-cdn-endpoint.png │ ├── configuring-cdn-endpoint.png │ ├── creating-storage-account.png │ ├── storage-container-policy.png │ └── umbraco-azure-filesystem-provider-install.png ├── Umbraco-Dashboard.md ├── Reference.md ├── Azure-Setup.md ├── Umbraco-Implementation.md ├── Our.md └── Umbraco-Setup.md ├── .editorconfig ├── src ├── Our.Umbraco.AzureCDNToolkit │ ├── App_Plugins │ │ └── AzureCDNToolkit │ │ │ ├── package.manifest │ │ │ ├── ImagePathCacheDashboard.css │ │ │ ├── ImagePathCacheDashboard.html │ │ │ └── ImagePathCacheDashboard.js │ ├── Models │ │ ├── CachedImagesWipe.cs │ │ ├── CachedImagesRequest.cs │ │ ├── CachedImage.cs │ │ └── CachedImagesResponse.cs │ ├── Constants.cs │ ├── Properties │ │ ├── VersionInfo.cs │ │ └── AssemblyInfo.cs │ ├── Our.Umbraco.AzureCDNToolkit.csproj.DotSettings │ ├── CacheRefreshers │ │ ├── CacheWiper.cs │ │ ├── CacheRequester.cs │ │ └── CacheResponder.cs │ ├── Events │ │ ├── ServerVariableParser.cs │ │ ├── UmbracoEvents.cs │ │ └── CacheEvents.cs │ ├── LocalCache.cs │ ├── RedisCache.cs │ ├── Extensions │ │ └── StringExtensions.cs │ ├── app.config │ ├── Cache.cs │ ├── packages.config │ ├── AzureCdnToolkit.cs │ ├── Controllers │ │ └── CacheApiController.cs │ ├── ImageCropperBaseExtensions.cs │ ├── ValueConverters │ │ └── RteValueConverter.cs │ ├── UrlHelperRenderExtensions.cs │ └── Our.Umbraco.AzureCDNToolkit.csproj └── Our.Umbraco.AzureCDNToolkit.Installer │ ├── Properties │ └── AssemblyInfo.cs │ ├── app.config │ ├── packages.config │ ├── PackageActions.cs │ └── Our.Umbraco.AzureCDNToolkit.Installer.csproj ├── NuGet.config ├── tests └── Our.Umbraco.AzureCDNToolkit.Tests │ ├── Properties │ └── AssemblyInfo.cs │ ├── app.config │ ├── packages.config │ ├── UrlHelperTests.cs │ ├── RedisCacheTests.cs │ └── Our.Umbraco.AzureCDNToolkit.Tests.csproj ├── appveyor.yml ├── .gitignore ├── AzureCDNToolkit.sln ├── README.md └── Rebracer.xml /docs/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrumpledDog/Umbraco-AzureCDNToolkit/HEAD/docs/images/dashboard.png -------------------------------------------------------------------------------- /docs/images/storage-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrumpledDog/Umbraco-AzureCDNToolkit/HEAD/docs/images/storage-keys.png -------------------------------------------------------------------------------- /docs/images/upload-assets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrumpledDog/Umbraco-AzureCDNToolkit/HEAD/docs/images/upload-assets.png -------------------------------------------------------------------------------- /docs/images/creating-cdn-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrumpledDog/Umbraco-AzureCDNToolkit/HEAD/docs/images/creating-cdn-profile.png -------------------------------------------------------------------------------- /docs/images/image-type-cropper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrumpledDog/Umbraco-AzureCDNToolkit/HEAD/docs/images/image-type-cropper.png -------------------------------------------------------------------------------- /docs/images/storage-containers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrumpledDog/Umbraco-AzureCDNToolkit/HEAD/docs/images/storage-containers.png -------------------------------------------------------------------------------- /docs/images/creating-cdn-endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrumpledDog/Umbraco-AzureCDNToolkit/HEAD/docs/images/creating-cdn-endpoint.png -------------------------------------------------------------------------------- /docs/images/configuring-cdn-endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrumpledDog/Umbraco-AzureCDNToolkit/HEAD/docs/images/configuring-cdn-endpoint.png -------------------------------------------------------------------------------- /docs/images/creating-storage-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrumpledDog/Umbraco-AzureCDNToolkit/HEAD/docs/images/creating-storage-account.png -------------------------------------------------------------------------------- /docs/images/storage-container-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrumpledDog/Umbraco-AzureCDNToolkit/HEAD/docs/images/storage-container-policy.png -------------------------------------------------------------------------------- /docs/images/umbraco-azure-filesystem-provider-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrumpledDog/Umbraco-AzureCDNToolkit/HEAD/docs/images/umbraco-azure-filesystem-provider-install.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | 9 | [*.{cs,cshtml}] 10 | indent_size = 4 -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/App_Plugins/AzureCDNToolkit/package.manifest: -------------------------------------------------------------------------------- 1 | { 2 | "javascript": [ 3 | "~/App_Plugins/AzureCDNToolkit/ImagePathCacheDashboard.js" 4 | ], 5 | "css": [ 6 | "~/App_Plugins/AzureCDNToolkit/ImagePathCacheDashboard.css" 7 | ] 8 | } -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Models/CachedImagesWipe.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.Models 2 | { 3 | using Newtonsoft.Json; 4 | public class CachedImagesWipe 5 | { 6 | [JsonProperty("weburl")] 7 | public string WebUrl { get; set; } 8 | 9 | [JsonProperty("serveridentity")] 10 | public string ServerIdentity { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit 2 | { 3 | internal static class Constants 4 | { 5 | internal static class Keys 6 | { 7 | internal const string CachePrefix = "Our.Umbraco.AzureCDNToolkit.ImageUrlCache_"; 8 | internal const string CachePrefixResponse = "Our.Umbraco.AzureCDNToolkit.CachedImageResponse_"; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Models/CachedImagesRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.Models 2 | { 3 | using System; 4 | 5 | using Newtonsoft.Json; 6 | public class CachedImagesRequest 7 | { 8 | [JsonProperty ("requestid")] 9 | public Guid RequestId { get; set; } 10 | 11 | [JsonProperty("serveridentity")] 12 | public string ServerIdentity { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Properties/VersionInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | // Version information for an assembly consists of the following four values: 4 | // 5 | // Major Version 6 | // Minor Version 7 | // Build Number 8 | // Revision 9 | // 10 | 11 | [assembly: AssemblyVersion("0.1.0.0")] 12 | [assembly: AssemblyFileVersion("0.1.0.0")] 13 | [assembly: AssemblyInformationalVersion("0.1.0-alpha-000001")] -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Models/CachedImage.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.Models 2 | { 3 | using Newtonsoft.Json; 4 | public class CachedImage 5 | { 6 | [JsonProperty ("weburl")] 7 | public string WebUrl { get; set; } 8 | [JsonProperty("cacheurl")] 9 | public string CacheUrl { get; set; } 10 | [JsonProperty("resolved")] 11 | public bool Resolved { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Our.Umbraco.AzureCDNToolkit.csproj.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | CSharp60 -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Models/CachedImagesResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.Models 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using Newtonsoft.Json; 6 | public class CachedImagesResponse 7 | { 8 | [JsonProperty ("requestid")] 9 | public Guid RequestId { get; set; } 10 | 11 | [JsonProperty("cachedimages")] 12 | public IEnumerable CachedImages { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/App_Plugins/AzureCDNToolkit/ImagePathCacheDashboard.css: -------------------------------------------------------------------------------- 1 | div.imagepathcachedash .table-striped tbody>tr:nth-child(odd)>td, div.imagepathcachedash>table-striped tbody>tr:nth-child(odd)>th { 2 | background-color: #f9f9f9; 3 | } 4 | 5 | div.imagepathcachedash .table tbody tr.warning>td { 6 | background-color: #fcf8e3; 7 | } 8 | 9 | div.imagepathcachedash .table tbody tr.success>td { 10 | background-color: #dff0d8; 11 | } 12 | 13 | div.imagepathcachedash .table tbody tr.error>td { 14 | background-color: #f2dede; 15 | } 16 | 17 | 18 | div.imagepathcachedash td { 19 | word-break: break-all; 20 | } -------------------------------------------------------------------------------- /docs/Umbraco-Dashboard.md: -------------------------------------------------------------------------------- 1 | # Umbraco Dashboard # 2 | 3 | Included with the AzureCDNToolkit is a dashboard that can be found in the developer section of Umbraco. 4 | 5 | This dashboard allows you to view the urls that have been resolved and cached to ImageProcessor.Web.Plugins.AzureBlobCache absolute urls. You can also see here the internal cachebusting going on. The dashboard does **not show asset urls** as they are not cached. 6 | 7 | The dashboard is load balancing aware so you can view the cache for any particular one server at a time. You can also wipe an entire cache for a particular server or a single cache item. 8 | 9 | ![Dashboard](images/dashboard.png) 10 | 11 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/CacheRefreshers/CacheWiper.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.CacheRefreshers 2 | { 3 | using System; 4 | using global::Umbraco.Core.Cache; 5 | 6 | public class CacheWiper : JsonCacheRefresherBase 7 | { 8 | public static Guid Guid 9 | { 10 | get { return new Guid("8882A4B1-69C5-4B41-B578-C65E6F630A97"); } 11 | } 12 | 13 | protected override CacheWiper Instance 14 | { 15 | get { return this; } 16 | } 17 | 18 | public override Guid UniqueIdentifier 19 | { 20 | get { return Guid; } 21 | } 22 | 23 | public override string Name 24 | { 25 | get { return "AzureCDNToolKitCacheWiper"; } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/CacheRefreshers/CacheRequester.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.CacheRefreshers 2 | { 3 | using System; 4 | using global::Umbraco.Core.Cache; 5 | public class CacheRequester : JsonCacheRefresherBase 6 | { 7 | public static Guid Guid 8 | { 9 | get { return new Guid("2A310ECC-D050-464D-9BED-2C9448255E01"); } 10 | } 11 | 12 | protected override CacheRequester Instance 13 | { 14 | get { return this; } 15 | } 16 | 17 | public override Guid UniqueIdentifier 18 | { 19 | get { return Guid; } 20 | } 21 | 22 | public override string Name 23 | { 24 | get { return "AzureCDNToolKitCacheReporter"; } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/CacheRefreshers/CacheResponder.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.CacheRefreshers 2 | { 3 | using System; 4 | using global::Umbraco.Core.Cache; 5 | 6 | public class CacheResponder : JsonCacheRefresherBase 7 | { 8 | public static Guid Guid 9 | { 10 | get { return new Guid("A4EDE1C6-C73B-4DB2-ADC9-23C22B2152F9"); } 11 | } 12 | 13 | protected override CacheResponder Instance 14 | { 15 | get { return this; } 16 | } 17 | 18 | public override Guid UniqueIdentifier 19 | { 20 | get { return Guid; } 21 | } 22 | 23 | public override string Name 24 | { 25 | get { return "AzureCDNToolKitCacheResponder"; } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Our.Umbraco.AzureCDNToolkit")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Our.Umbraco.AzureCDNToolkit")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("0fd015d8-9060-4d5f-a722-e07b65a5ee35")] 24 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Events/ServerVariableParser.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.Events 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | using System.Web; 7 | using System.Web.Mvc; 8 | using System.Web.Routing; 9 | 10 | using global::Umbraco.Core; 11 | using global::Umbraco.Web; 12 | using global::Umbraco.Web.UI.JavaScript; 13 | 14 | using Controllers; 15 | 16 | public class ServerVariableParser : ApplicationEventHandler 17 | { 18 | protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) 19 | { 20 | ServerVariablesParser.Parsing += ServerVariablesParser_Parsing; 21 | } 22 | 23 | void ServerVariablesParser_Parsing(object sender, Dictionary e) 24 | { 25 | if (HttpContext.Current == null) return; 26 | var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); 27 | 28 | var mainDictionary = new Dictionary 29 | { 30 | { 31 | "cacheApiBaseUrl", 32 | urlHelper.GetUmbracoApiServiceBaseUrl(controller => controller.GetAllServers()) 33 | } 34 | }; 35 | 36 | if (!e.Keys.Contains("azureCdnToolkitUrls")) 37 | { 38 | e.Add("azureCdnToolkitUrls", mainDictionary); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Our.Umbraco.AzureCDNToolkit.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Our.Umbraco.AzureCDNToolkit.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Our.Umbraco.AzureCDNToolkit.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("b5f8514e-6d0f-42c6-86e0-3aa278f31506")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit.Installer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Our.Umbraco.AzureCDNToolkit.Installer")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Our.Umbraco.AzureCDNToolkit.Installer")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("2587066d-369e-45bd-9dfd-028920570f73")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/LocalCache.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | using global::Umbraco.Core; 7 | using global::Umbraco.Core.Cache; 8 | 9 | internal static class LocalCache 10 | { 11 | internal static T GetLocalCacheItem(string cacheKey) 12 | { 13 | var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; 14 | var cachedItem = runtimeCache.GetCacheItem(cacheKey); 15 | return cachedItem; 16 | } 17 | 18 | internal static void InsertLocalCacheItem(string cacheKey, Func getCacheItem) 19 | { 20 | var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; 21 | runtimeCache.InsertCacheItem(cacheKey, getCacheItem); 22 | } 23 | 24 | internal static IEnumerable GetLocalCacheItemsByKeySearch(string keyStartsWith) 25 | { 26 | var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; 27 | return runtimeCache.GetCacheItemsByKeySearch(keyStartsWith); 28 | } 29 | 30 | internal static void ClearLocalCacheItem(string key) 31 | { 32 | var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; 33 | runtimeCache.ClearCacheItem(key); 34 | } 35 | 36 | internal static void ClearLocalCacheByKeySearch(string keyStartsWith) 37 | { 38 | var runtimeCache = ApplicationContext.Current.ApplicationCache.RuntimeCache; 39 | runtimeCache.ClearCacheByKeySearch(keyStartsWith); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | os: Visual Studio 2015 2 | 3 | # Version format 4 | version: 0.2.0.{build} 5 | 6 | cache: 7 | - packages -> **\packages.config # preserve "packages" directory in the root of build folder but will reset it if packages.config is modified 8 | 9 | branches: 10 | only: 11 | - develop 12 | - master 13 | 14 | # UMBRACO_PACKAGE_PRERELEASE_SUFFIX if a rtm release build this should be blank, otherwise if empty will default to alpha 15 | # example UMBRACO_PACKAGE_PRERELEASE_SUFFIX=beta 16 | init: 17 | - set UMBRACO_PACKAGE_PRERELEASE_SUFFIX=alpha 18 | 19 | build_script: 20 | - build-appveyor.cmd 21 | 22 | artifacts: 23 | - path: artifacts\*.nupkg 24 | - path: artifacts\*.zip 25 | 26 | deploy: 27 | # Umbraco MyGet community feed 28 | - provider: NuGet 29 | server: https://www.myget.org/F/umbraco-packages/api/v2/package 30 | symbol_server: https://nuget.symbolsource.org/MyGet/umbraco-packages 31 | api_key: 32 | secure: SASQGWG/4zNns7bwSSsJ5RPvKcKfJsBeEPuw69wsVPA3PO739QmzVtc5VwQwgvbr 33 | artifact: /.*\.nupkg/ 34 | on: 35 | branch: develop 36 | 37 | # GitHub Deployment for releases 38 | - provider: GitHub 39 | auth_token: 40 | secure: c0LwOGqoFZIieyy8CHrUtYQOD0HL0rG5tV2DS+8FHv34BKs/LSGGtmWWep5O7GbV 41 | artifact: /.*\.zip/ # upload all Zip packages to release assets 42 | draft: false 43 | prerelease: false 44 | on: 45 | branch: master 46 | appveyor_repo_tag: true # deploy on tag push only 47 | 48 | # NuGet Deployment for releases 49 | - provider: NuGet 50 | server: 51 | api_key: 52 | secure: adk3pI9HCByZg2VRxplX9UR6qHf97bA3as6dusxLswCKI8BQQE8DO5a0frLrI+EO 53 | artifact: /.*\.nupkg/ 54 | on: 55 | branch: master 56 | appveyor_repo_tag: true -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/App_Plugins/AzureCDNToolkit/ImagePathCacheDashboard.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

AzureCDNToolkit: Cached Image Path Status

5 |
6 | 7 |
8 |
9 | 15 | 16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
WebUrlCacheUrlResolved
No cached image paths found
Please wait, it can take serveral minutes for servers to respond...
{{ image.weburl }}{{ image.cacheurl }}{{ image.resolved }}
56 |
57 | 58 |
-------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Events/UmbracoEvents.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.Events 2 | { 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Configuration; 6 | 7 | using global::Umbraco.Core; 8 | using global::Umbraco.Core.Security; 9 | using global::Umbraco.Core.PropertyEditors; 10 | using global::Umbraco.Web.PropertyEditors.ValueConverters; 11 | 12 | using ImageProcessor.Web.HttpModules; 13 | public class UmbracoEvents : ApplicationEventHandler 14 | { 15 | protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) 16 | { 17 | PropertyValueConvertersResolver.Current.RemoveType(); 18 | } 19 | 20 | protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) 21 | { 22 | ImageProcessingModule.ValidatingRequest += ImageProcessingModule_ValidatingRequest; 23 | } 24 | 25 | private void ImageProcessingModule_ValidatingRequest(object sender, ImageProcessor.Web.Helpers.ValidatingRequestEventArgs args) 26 | { 27 | var securityToken = WebConfigurationManager.AppSettings["AzureCDNToolkit:SecurityToken"]; 28 | var securityModeEnabled = bool.Parse(WebConfigurationManager.AppSettings["AzureCDNToolkit:SecurityModeEnabled"]); 29 | 30 | if (securityModeEnabled && !string.IsNullOrWhiteSpace(args.QueryString) && !string.IsNullOrEmpty(securityToken)) 31 | { 32 | var queryCollection = HttpUtility.ParseQueryString(args.QueryString); 33 | 34 | // if token is not present or value doesn't match then we can cancel the request 35 | if (!queryCollection.AllKeys.Contains("securitytoken") || queryCollection["securitytoken"] != securityToken) 36 | { 37 | // we can allow on-demand image processor requests if the user has a umbraco auth ticket which means they are logged into Umbraco for things like grid editor previews 38 | var ticket = new HttpContextWrapper(HttpContext.Current).GetUmbracoAuthTicket(); 39 | if (ticket == null) 40 | { 41 | args.Cancel = true; 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit.Installer/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/RedisCache.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using Newtonsoft.Json; 7 | using StackExchange.Redis; 8 | 9 | public class RedisCache 10 | { 11 | private static readonly Lazy LazyConnection = new Lazy(() => 12 | { 13 | return ConnectionMultiplexer.Connect(AzureCdnToolkit.Instance.RedisCacheConnectionString); 14 | }); 15 | 16 | public static ConnectionMultiplexer Connection => LazyConnection.Value; 17 | 18 | public static IDatabase Db => Connection.GetDatabase(); 19 | 20 | public static T GetCacheItem(string cacheKey) 21 | { 22 | var cacheItem = Db.StringGet(cacheKey); 23 | 24 | if (cacheItem.HasValue) 25 | { 26 | return JsonConvert.DeserializeObject(cacheItem.ToString()); 27 | } 28 | 29 | return default(T); 30 | } 31 | 32 | public static void InsertCacheItem(string cacheKey, T item) 33 | { 34 | string cacheValue = JsonConvert.SerializeObject(item); 35 | 36 | Db.StringSet(cacheKey, cacheValue); 37 | } 38 | 39 | public static IEnumerable GetCacheItemsByKeySearch(string keyStartsWith) 40 | { 41 | var endpoints = Connection.GetEndPoints(); 42 | var server = Connection.GetServer(endpoints.First()); 43 | var keys = server.Keys(pattern: keyStartsWith + "*"); 44 | 45 | return keys.Select(key => JsonConvert.DeserializeObject(Db.StringGet(key).ToString())).ToList(); 46 | } 47 | 48 | public static void ClearCacheItem(string cacheKey) 49 | { 50 | Db.KeyDelete(cacheKey); 51 | } 52 | 53 | public static void ClearCache() 54 | { 55 | var endpoints = Connection.GetEndPoints(); 56 | var server = Connection.GetServer(endpoints.First()); 57 | server.FlushAllDatabases(); 58 | } 59 | 60 | public static void ClearCacheByKeySearch(string keyStartsWith) 61 | { 62 | var endpoints = Connection.GetEndPoints(); 63 | var server = Connection.GetServer(endpoints.First()); 64 | var keys = server.Keys(pattern: keyStartsWith + "*"); 65 | 66 | foreach (var key in keys) 67 | { 68 | Db.KeyDelete(key); 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | using System.Threading.Tasks; 7 | 8 | namespace Our.Umbraco.AzureCDNToolkit.Extensions 9 | { 10 | public static class StringExtensions 11 | { 12 | public static string[] ParseExact( 13 | this string data, 14 | string format) 15 | { 16 | return ParseExact(data, format, false); 17 | } 18 | 19 | public static string[] ParseExact( 20 | this string data, 21 | string format, 22 | bool ignoreCase) 23 | { 24 | string[] values; 25 | 26 | if (TryParseExact(data, format, out values, ignoreCase)) 27 | return values; 28 | else 29 | throw new ArgumentException("Format not compatible with value."); 30 | } 31 | 32 | public static bool TryExtract( 33 | this string data, 34 | string format, 35 | out string[] values) 36 | { 37 | return TryParseExact(data, format, out values, false); 38 | } 39 | 40 | public static bool TryParseExact( 41 | this string data, 42 | string format, 43 | out string[] values, 44 | bool ignoreCase) 45 | { 46 | int tokenCount = 0; 47 | format = Regex.Escape(format).Replace("\\{", "{"); 48 | 49 | for (tokenCount = 0; ; tokenCount++) 50 | { 51 | string token = string.Format("{{{0}}}", tokenCount); 52 | if (!format.Contains(token)) break; 53 | format = format.Replace(token, 54 | string.Format("(?'group{0}'.*)", tokenCount)); 55 | } 56 | 57 | RegexOptions options = 58 | ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None; 59 | 60 | Match match = new Regex(format, options).Match(data); 61 | 62 | if (tokenCount != (match.Groups.Count - 1)) 63 | { 64 | values = new string[] { }; 65 | return false; 66 | } 67 | else 68 | { 69 | values = new string[tokenCount]; 70 | for (int index = 0; index < tokenCount; index++) 71 | values[index] = 72 | match.Groups[string.Format("group{0}", index)].Value; 73 | return true; 74 | } 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /docs/Reference.md: -------------------------------------------------------------------------------- 1 | # Reference # 2 | 3 | ## GetCropCdnUrl ## 4 | 5 | ### IPublishedContent ### 6 | 7 | This method is a direct replacement of the Umbraco UrlHelper `GetCropUrl` method 8 | 9 | IHtmlString GetCropCdnUrl(this UrlHelper urlHelper, 10 | IPublishedContent mediaItem, 11 | int? width = null, 12 | int? height = null, 13 | string propertyAlias = global::Umbraco.Core.Constants.Conventions.Media.File, 14 | string cropAlias = null, 15 | int? quality = null, 16 | ImageCropMode? imageCropMode = null, 17 | ImageCropAnchor? imageCropAnchor = null, 18 | bool preferFocalPoint = false, 19 | bool useCropDimensions = false, 20 | bool cacheBuster = true, 21 | string furtherOptions = null, 22 | ImageCropRatioMode? ratioMode = null, 23 | bool upScale = true, 24 | bool htmlEncode = true 25 | ) 26 | 27 | ### ImageCropDataSet ### 28 | 29 | This method is useful if you have strongly typed models (e.g. Ditto) with ImageCropDataSet image cropper properties. 30 | 31 | IHtmlString GetCropCdnUrl(this UrlHelper urlHelper, 32 | ImageCropDataSet imageCropper, 33 | int? width = null, 34 | int? height = null, 35 | string propertyAlias = global::Umbraco.Core.Constants.Conventions.Media.File, 36 | string cropAlias = null, 37 | int? quality = null, 38 | ImageCropMode? imageCropMode = null, 39 | ImageCropAnchor? imageCropAnchor = null, 40 | bool preferFocalPoint = false, 41 | bool useCropDimensions = false, 42 | string cacheBusterValue = null, 43 | string furtherOptions = null, 44 | ImageCropRatioMode? ratioMode = null, 45 | bool upScale = true, 46 | bool htmlEncode = true 47 | ) 48 | 49 | If possible you should try to supply a cacheBusterValue ideally derived from the Umbraco UpdateDate. 50 | 51 | e.g. 52 | 53 | var cacheBusterValue = MyModel.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture); 54 | 55 | ## ResolveCdn ## 56 | 57 | ### IPublishedContent ### 58 | 59 | IHtmlString ResolveCdn(this UrlHelper urlHelper, IPublishedContent mediaItem, bool asset = true, string querystring = null, bool htmlEncode = true) 60 | 61 | ### String ### 62 | 63 | IHtmlString ResolveCdn(this UrlHelper urlHelper, string path, bool asset = true, bool htmlEncode = true) 64 | 65 | ### String Advanced ### 66 | 67 | IHtmlString ResolveCdn(this UrlHelper urlHelper, string path, string cacheBuster, bool asset = true, string cacheBusterName = "v", bool htmlEncode = true) -------------------------------------------------------------------------------- /tests/Our.Umbraco.AzureCDNToolkit.Tests/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/Azure-Setup.md: -------------------------------------------------------------------------------- 1 | # Azure Setup # 2 | 3 | ## 1. Storage account ## 4 | 5 | To use the Azure CDN Toolkit you will need an Azure Storage account, you create this using the [Azure Portal](https://portal.azure.com/) You will need to create a Resource Group if you don't already have one. 6 | 7 | ![Creating Storage Account](images/creating-storage-account.png) 8 | 9 | ## 2. Get your access keys ## 10 | 11 | You will need both the storage account name and a access key for configuration later on. 12 | 13 | ![Storage Keys](images/storage-keys.png) 14 | 15 | ## 3. Creating a CDN Profile ## 16 | 17 | Use the same Resource Group as you created the Storage Account in. 18 | 19 | ![Creating CDN Profile](images/creating-cdn-profile.png) 20 | 21 | ## 4. Creating a CDN Endpoint ## 22 | 23 | Once the CDN profile is created you can then create your endpoint which will use the Storage Account created in Step 1. 24 | 25 | Select "Storage" as the origin type and select your Storage Account created in Step 1 as the origin hostname. 26 | 27 | ![Creating CDN Endpoint](images/creating-cdn-endpoint.png) 28 | 29 | ## 5. Configure the CDN endpoint ## 30 | 31 | Once the CDN endpoint is create you can then configure it. 32 | 33 | 1. Turn compression On 34 | 2. Add the following formats to compress 35 | - image/jpeg 36 | - image/pjpeg 37 | - image/png 38 | - image/gif 39 | 3. Select "Cache every unique URL" in the "Query string caching behaviour" drop down 40 | 4. Click save 41 | 42 | ![Configuring CDN Endpoint](images/configuring-cdn-endpoint.png) 43 | 44 | ## 6. Very patient (very) ## 45 | 46 | The portal will tell you that your endpoint will be available in 90 minutes, often it takes a lot long than this. If you can set this up 24 hours in advance of needing the CDN to be fully operational. 47 | 48 | ## 7. Create your storage containers ## 49 | 50 | The default setup for the Azure CDN toolkit is to have three containers in your storage account. 51 | 52 | - assets 53 | - cloudcache 54 | - media 55 | 56 | You only have to create the assets container manually as the others will be create automatically but you may choose to create them manually if you want to. 57 | 58 | You will need to use an Azure Storage Explorer to create and manage your blobs, there are various tools for this and they have been compared in detail in this [blog article](https://azure.microsoft.com/en-us/documentation/articles/storage-explorers/). The screenshots in this documentation are from ClumsyLeaf CloudXplorer. 59 | 60 | ![Storage Containers](images/storage-containers.png) 61 | 62 | When creating a container it's **important** to ensure that the container policy is set to "Public read access (blobs only)" otherwise the CDN will not be able to serve content from the container. 63 | 64 | ![Storage Container Policy](images/storage-container-policy.png) 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /tests/Our.Umbraco.AzureCDNToolkit.Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit.Installer/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/Umbraco-Implementation.md: -------------------------------------------------------------------------------- 1 | # Umbraco Implementation # 2 | 3 | Now you have everything setup and installed you are ready to begin using the Umbraco Azure CDN Toolkit. 4 | 5 | ## 1. Cropped images ## 6 | 7 | Where you would normally use the Umbraco `GetCropUrl` UrlHelper method instead you now use `GetCropCdnUrl`. 8 | 9 | e.g. 10 | 11 | 12 | 13 | change to: 14 | 15 | 16 | 17 | With the toolkit enabled, you should now see your cropped image paths change to absolute CDN references. 18 | 19 | e.g. 20 | 21 | 22 | 23 | change to: 24 | 25 | 26 | 27 | If you turn off the toolkit in web.config it should render as it would if you had used `GetCropUrl` directly, useful for development! 28 | 29 | Internally cache busting has been added, you can view this in the [dashboard](Umbraco-Dashboard.md). 30 | 31 | The toolkit also adds a UrlHelper `GetCropCdnUrl` method with a first parameter of type `ImageCropDataSet` for use with your own strongly typed models or mappers such as Ditto. See the [reference documentation](Reference.md) for more info 32 | 33 | ## 2. Static assets ## 34 | 35 | Where you would normally reference css, js or static images you can now use a UrlHelper method called `ResolveCdn`. 36 | 37 | e.g. 38 | 39 | 40 | 41 | change to: 42 | 43 | 44 | 45 | With the toolkit enabled, you should now see your asset paths change to absolute CDN reference. 46 | 47 | e.g. 48 | 49 | 50 | 51 | You can see that the toolkit has also added that all important cachebuster variable. 52 | 53 | ## 3. Downloads ## 54 | 55 | Where you might normally render a link to download a pdf or docx stored in Umbraco media you can now use the `ResolveCdn` UrlHelper 56 | 57 | e.g. 58 | 59 | Download Word Doc 60 | 61 | change to: 62 | 63 | Download Word Doc 64 | 65 | With the toolkit enabled, you should now see your media paths change to absolute CDN reference. 66 | 67 | e.g. 68 | 69 | Download Word Doc 70 | 71 | You can see that the toolkit has also added that all important cachebuster variable. 72 | 73 | ## 4. Static assets with ImageProcessor.Web commands ## 74 | 75 | Sometimes there might be some images with ImageProcessor.Web commands. 76 | 77 | e.g. 78 | 79 | 80 | 81 | change to: 82 | 83 | 84 | 85 | With the toolkit enabled, you should now see your image paths change to absolute CDN reference, the result will of course be the output from ImageProcessor.Web 86 | 87 | e.g. 88 | 89 | 90 | 91 | Internally cache busting has been added, you can view this in the [dashboard](Umbraco-Dashboard.md). 92 | -------------------------------------------------------------------------------- /docs/Our.md: -------------------------------------------------------------------------------- 1 | The AzureCDNToolkit package allows you to fully utilise and integrate the Azure CDN with your Umbraco powered website using a "Origin Push" approach. 2 | 3 | **To Push or to Pull** 4 | You can use both a Push or a Pull origin approach to the Azure CDN with Umbraco since the release of the new Azure CDN (AzureEdge), this package supports only Push origin (storage) currently however it maybe extended to also support Pull in the future. 5 | 6 | There are three file types that should be served from CDN if you have one. 7 | 8 | - Assets - css, js & static images used by templates etc.. 9 | - Images managed by Umbraco - cropped or not 10 | - Files - pdfs, docx etc 11 | 12 | The toolkit depends on two packages being installed, these are the [UmbracoFileSystemProviders.Azure](https://github.com/JimBobSquarePants/UmbracoFileSystemProviders.Azure) and the [ImageProcessor.Web Azure Blob Cache plugin](http://imageprocessor.org/imageprocessor-web/plugins/azure-blob-cache/) 13 | 14 | Once installed and setup the package provides UrlHelper methods to use for resolving url paths for assets and Image Cropper urls and also a value converter for the TinyMce editor so that images within the content are also resolved. 15 | 16 | Some examples: 17 | 18 | @Url.ResolveCdn("/css/style.css") 19 | 20 | @Url.GetCropCdnUrl(Umbraco.TypedMedia(1084), width: 150) 21 | 22 |
23 | 24 | When using these methods, the toolkit will attempt to resolve the urls to their **absolute** paths and **crucially** ensures that a cache busting querystring variable is added. Without the cache busting using the Azure CDN can become tricky when you want to update with new content. This has the added benefit of avoiding your site needing to handle any 301 redirects and also gets some optimisation benefit (PageSpeed etc). 25 | 26 | Some examples: 27 | 28 | 1. `` 29 | 2. `` 30 | 3. `Download Word Doc` 31 | 32 | Becomes: 33 | 34 | 1. `` 35 | 2. `` 36 | 3. `Download Word Doc` 37 | 38 | **Note**: this package is not compatible with [Slimsy](https://our.umbraco.org/projects/website-utilities/slimsy/) v1 (it should work with the upcoming Slimsy v2 using img srcset) 39 | 40 | ## NuGet ## 41 | 42 | It is highly recommended that you install this package via NuGet: 43 | 44 | Install-Package Our.Umbraco.AzureCDNToolkit -Pre 45 | 46 | ## Documentation ## 47 | 48 | 1. [Azure Setup](https://github.com/CrumpledDog/Umbraco-AzureCDNToolkit/blob/develop/docs/Azure-Setup.md) 49 | 2. [Umbraco Setup](https://github.com/CrumpledDog/Umbraco-AzureCDNToolkit/blob/develop/docs/Umbraco-Setup.md) 50 | 3. [Umbraco Implementation](https://github.com/CrumpledDog/Umbraco-AzureCDNToolkit/blob/develop/docs/Umbraco-Implementation.md) 51 | 4. [Umbraco Dashboard](https://github.com/CrumpledDog/Umbraco-AzureCDNToolkit/blob/develop/docs/Umbraco-Dashboard.md) 52 | 5. [Reference](https://github.com/CrumpledDog/Umbraco-AzureCDNToolkit/blob/develop/docs/Reference.md) -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Cache.cs: -------------------------------------------------------------------------------- 1 | using Umbraco.Core.Logging; 2 | 3 | namespace Our.Umbraco.AzureCDNToolkit 4 | { 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | public static class Cache 9 | { 10 | public static T GetCacheItem(string cacheKey) 11 | { 12 | if (AzureCdnToolkit.Instance.UseRedisCache) 13 | { 14 | try 15 | { 16 | return RedisCache.GetCacheItem(cacheKey); 17 | } 18 | catch (Exception e) 19 | { 20 | // log and fall back to local cache 21 | LogHelper.Error(e.Message, e); 22 | } 23 | } 24 | 25 | return LocalCache.GetLocalCacheItem(cacheKey); 26 | } 27 | 28 | public static void InsertCacheItem(string cacheKey, Func getCacheItem) 29 | { 30 | if (AzureCdnToolkit.Instance.UseRedisCache) 31 | { 32 | try 33 | { 34 | var item = getCacheItem(); 35 | RedisCache.InsertCacheItem(cacheKey, item); 36 | return; 37 | } 38 | catch (Exception e) 39 | { 40 | // log and fall back to local cache 41 | LogHelper.Error(e.Message, e); 42 | } 43 | } 44 | 45 | LocalCache.InsertLocalCacheItem(cacheKey, getCacheItem); 46 | } 47 | 48 | public static IEnumerable GetCacheItemsByKeySearch(string keyStartsWith) 49 | { 50 | if (AzureCdnToolkit.Instance.UseRedisCache) 51 | { 52 | try 53 | { 54 | return RedisCache.GetCacheItemsByKeySearch(keyStartsWith); 55 | } 56 | catch (Exception e) 57 | { 58 | // log and fall back to local cache 59 | LogHelper.Error(e.Message, e); 60 | } 61 | } 62 | 63 | return LocalCache.GetLocalCacheItemsByKeySearch(keyStartsWith); 64 | } 65 | 66 | public static void ClearCacheItem(string key) 67 | { 68 | if (AzureCdnToolkit.Instance.UseRedisCache) 69 | { 70 | try 71 | { 72 | RedisCache.ClearCacheItem(key); 73 | return; 74 | } 75 | catch (Exception e) 76 | { 77 | // log and fall back to local cache 78 | LogHelper.Error(e.Message, e); 79 | } 80 | } 81 | 82 | LocalCache.ClearLocalCacheItem(key); 83 | } 84 | 85 | public static void ClearCacheByKeySearch(string keyStartsWith) 86 | { 87 | if (AzureCdnToolkit.Instance.UseRedisCache) 88 | { 89 | try 90 | { 91 | RedisCache.ClearCacheByKeySearch(keyStartsWith); 92 | return; 93 | } 94 | catch (Exception e) 95 | { 96 | // log and fall back to local cache 97 | LogHelper.Error(e.Message, e); 98 | } 99 | } 100 | 101 | LocalCache.ClearLocalCacheByKeySearch(keyStartsWith); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | #VS 2015 hidden folder 5 | .vs/* 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.sln.docstates 11 | 12 | # Build results 13 | 14 | [Dd]ebug/ 15 | [Rr]elease/ 16 | x64/ 17 | [Oo]bj/ 18 | 19 | # MSTest test Results 20 | [Tt]est[Rr]esult*/ 21 | [Bb]uild[Ll]og.* 22 | 23 | *_i.c 24 | *_p.c 25 | *.ilk 26 | *.meta 27 | *.obj 28 | *.pch 29 | *.pdb 30 | *.pgc 31 | *.pgd 32 | *.rsp 33 | *.sbr 34 | *.tlb 35 | *.tli 36 | *.tlh 37 | *.tmp 38 | *.tmp_proj 39 | *.log 40 | *.vspscc 41 | *.vssscc 42 | .builds 43 | *.pidb 44 | *.log 45 | *.scc 46 | 47 | # Visual C++ cache files 48 | ipch/ 49 | *.aps 50 | *.ncb 51 | *.opensdf 52 | #*.sdf 53 | *.cachefile 54 | 55 | # Visual Studio profiler 56 | *.psess 57 | *.vsp 58 | *.vspx 59 | 60 | # Guidance Automation Toolkit 61 | *.gpState 62 | 63 | # ReSharper is a .NET coding add-in 64 | _ReSharper*/ 65 | *.[Rr]e[Ss]harper 66 | 67 | # TeamCity is a build add-in 68 | _TeamCity* 69 | 70 | # DotCover is a Code Coverage Tool 71 | *.dotCover 72 | 73 | # NCrunch 74 | *.ncrunch* 75 | .*crunch*.local.xml 76 | 77 | # Installshield output folder 78 | [Ee]xpress/ 79 | 80 | # DocProject is a documentation generator add-in 81 | DocProject/buildhelp/ 82 | DocProject/Help/*.HxT 83 | DocProject/Help/*.HxC 84 | DocProject/Help/*.hhc 85 | DocProject/Help/*.hhk 86 | DocProject/Help/*.hhp 87 | DocProject/Help/Html2 88 | DocProject/Help/html 89 | 90 | # Click-Once directory 91 | publish/ 92 | 93 | # Publish Web Output 94 | *.Publish.xml 95 | 96 | # NuGet Packages Directory 97 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 98 | packages/ 99 | 100 | !src/TestSite/App_Data/packages 101 | !src/TestSite/Umbraco/Developer/Packages 102 | 103 | # Windows Azure Build Output 104 | csx 105 | *.build.csdef 106 | 107 | # Windows Store app package directory 108 | AppPackages/ 109 | 110 | # Others 111 | sql/ 112 | *.Cache 113 | ClientBin/ 114 | [Ss]tyle[Cc]op.* 115 | ~$* 116 | *~ 117 | *.dbmdl 118 | *.[Pp]ublish.xml 119 | *.pfx 120 | *.publishsettings 121 | 122 | # RIA/Silverlight projects 123 | Generated_Code/ 124 | 125 | # Backup & report files from converting an old project file to a newer 126 | # Visual Studio version. Backup files are not needed, because we have git ;-) 127 | _UpgradeReport_Files/ 128 | Backup*/ 129 | UpgradeLog*.XML 130 | UpgradeLog*.htm 131 | 132 | # SQL Server files 133 | #App_Data/*.mdf 134 | #App_Data/*.ldf 135 | 136 | 137 | #LightSwitch generated files 138 | GeneratedArtifacts/ 139 | _Pvt_Extensions/ 140 | ModelManifest.xml 141 | 142 | # ========================= 143 | # Windows detritus 144 | # ========================= 145 | 146 | # Windows image file caches 147 | Thumbs.db 148 | ehthumbs.db 149 | 150 | # Folder config file 151 | Desktop.ini 152 | 153 | # Recycle Bin used on file shares 154 | $RECYCLE.BIN/ 155 | 156 | # Mac desktop service store files 157 | .DS_Store 158 | 159 | # ========================= 160 | # Umbraco media 161 | # ========================= 162 | src/TestSite/media/* 163 | 164 | # ========================= 165 | # Files in bin folders 166 | # ========================= 167 | **/[Bb]in/* 168 | 169 | # ========================= 170 | # Umbraco 171 | # ========================= 172 | src/TestSite/**/* 173 | 174 | AzureCDNToolkitWithTestSite.sln 175 | 176 | # ========================= 177 | # ImageProcessor DiskCache 178 | # ========================= 179 | src/TestSite/App_Data/cache/* 180 | 181 | artifacts/** 182 | 183 | *.map 184 | *.orig 185 | *.rej -------------------------------------------------------------------------------- /tests/Our.Umbraco.AzureCDNToolkit.Tests/UrlHelperTests.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.Tests 2 | { 3 | using System.Web.Mvc; 4 | using NUnit.Framework; 5 | 6 | [TestFixture] 7 | public class UrlHelperTests 8 | { 9 | [Test] 10 | public void TestAbsoluteUrl() 11 | { 12 | AzureCdnToolkit.Instance.Refresh(); 13 | 14 | var url = "https://i.ytimg.com/vi/mW3S0u8bj58/maxresdefault.jpg"; 15 | var expected ="https://i.ytimg.com/vi/mW3S0u8bj58/maxresdefault.jpg"; 16 | 17 | var resolvedUrl = new UrlHelper().ResolveCdn(url).ToString(); 18 | Assert.AreEqual(expected, resolvedUrl); 19 | } 20 | 21 | [Test] 22 | public void TestAbsoluteUrlWithQuerystring() 23 | { 24 | AzureCdnToolkit.Instance.Refresh(); 25 | 26 | var url = "https://i.ytimg.com/vi/mW3S0u8bj58/maxresdefault.jpg?v=12345"; 27 | var expected = "https://i.ytimg.com/vi/mW3S0u8bj58/maxresdefault.jpg?v=12345"; 28 | 29 | var resolvedUrl = new UrlHelper().ResolveCdn(url).ToString(); 30 | Assert.AreEqual(expected, resolvedUrl); 31 | } 32 | 33 | [Test] 34 | public void TestRelativeMediaUrlNaked() 35 | { 36 | AzureCdnToolkit.Instance.Refresh(); 37 | 38 | var url = "/media/1051/church.jpg"; 39 | var expected = "https://azurecdntoolkitdemo.blob.core.windows.net/media/1051/church.jpg?v=0.0.1"; 40 | 41 | var resolvedUrl = new UrlHelper().ResolveCdn(url, asset:false).ToString(); 42 | Assert.AreEqual(expected, resolvedUrl); 43 | } 44 | 45 | [Test] 46 | public void TestRelativeMediaUrlWithCacheBuster() 47 | { 48 | AzureCdnToolkit.Instance.Refresh(); 49 | 50 | var url = "/media/1051/church.jpg?rnd=12122121112"; 51 | var expected = "https://azurecdntoolkitdemo.blob.core.windows.net/media/1051/church.jpg?rnd=12122121112"; 52 | 53 | var resolvedUrl = new UrlHelper().ResolveCdn(url, asset: false, cacheBuster:"12122121112").ToString(); 54 | Assert.AreEqual(expected, resolvedUrl); 55 | } 56 | 57 | [Test] 58 | public void TestRelativeMediaUrlNakedCustomContainer() 59 | { 60 | AzureCdnToolkit.Instance.Refresh(); 61 | AzureCdnToolkit.Instance.MediaContainer = "myspecialcontainer"; 62 | var url = "/media/1051/church.jpg"; 63 | var expected = "https://azurecdntoolkitdemo.blob.core.windows.net/myspecialcontainer/1051/church.jpg?v=0.0.1"; 64 | 65 | var resolvedUrl = new UrlHelper().ResolveCdn(url, asset: false).ToString(); 66 | Assert.AreEqual(expected, resolvedUrl); 67 | } 68 | 69 | [Test] 70 | public void TestRelativeMediaUrlWithQuerystring() 71 | { 72 | AzureCdnToolkit.Instance.Refresh(); 73 | 74 | var url = "/media/1051/church.jpg?width=100"; 75 | var expected = "/media/1051/church.jpg?width=100&v=0.0.1"; 76 | 77 | var resolvedUrl = new UrlHelper().ResolveCdn(url, asset: false, htmlEncode:false).ToString(); 78 | Assert.AreEqual(expected, resolvedUrl); 79 | } 80 | 81 | [Test] 82 | public void TestRelativeAssetUrlNaked() 83 | { 84 | AzureCdnToolkit.Instance.Refresh(); 85 | 86 | var url = "/css/mycss.css"; 87 | var expected = "https://azurecdntoolkitdemo.blob.core.windows.net/assets/css/mycss.css?v=0.0.1"; 88 | 89 | var resolvedUrl = new UrlHelper().ResolveCdn(url).ToString(); 90 | Assert.AreEqual(expected, resolvedUrl); 91 | } 92 | 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit.Installer/PackageActions.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.Installer 2 | { 3 | using System; 4 | using System.Web; 5 | 6 | using Microsoft.Web.XmlTransform; 7 | 8 | using umbraco.cms.businesslogic.packager.standardPackageActions; 9 | using umbraco.interfaces; 10 | using global::Umbraco.Core.Logging; 11 | public class PackageActions 12 | { 13 | public class TransformConfig : IPackageAction 14 | { 15 | public string Alias() 16 | { 17 | return "AzureCDNToolkit.TransformConfig"; 18 | } 19 | 20 | public bool Execute(string packageName, System.Xml.XmlNode xmlData) 21 | { 22 | return this.Transform(packageName, xmlData); 23 | } 24 | 25 | public System.Xml.XmlNode SampleXml() 26 | { 27 | var str = "" + 28 | ""; 29 | return helper.parseStringToXmlNode(str); 30 | } 31 | 32 | public bool Undo(string packageName, System.Xml.XmlNode xmlData) 33 | { 34 | return this.Transform(packageName, xmlData, true); 35 | } 36 | 37 | private bool Transform(string packageName, System.Xml.XmlNode xmlData, bool uninstall = false) 38 | { 39 | // The config file we want to modify 40 | if (xmlData.Attributes != null) 41 | { 42 | var file = xmlData.Attributes.GetNamedItem("file").Value; 43 | 44 | var sourceDocFileName = VirtualPathUtility.ToAbsolute(file); 45 | 46 | // The xdt file used for tranformation 47 | var fileEnd = "install.xdt"; 48 | if (uninstall) 49 | { 50 | fileEnd = string.Format("un{0}", fileEnd); 51 | } 52 | 53 | var xdtfile = string.Format("{0}.{1}", xmlData.Attributes.GetNamedItem("xdtfile").Value, fileEnd); 54 | var xdtFileName = VirtualPathUtility.ToAbsolute(xdtfile); 55 | 56 | // The translation at-hand 57 | using (var xmlDoc = new XmlTransformableDocument()) 58 | { 59 | xmlDoc.PreserveWhitespace = true; 60 | xmlDoc.Load(HttpContext.Current.Server.MapPath(sourceDocFileName)); 61 | 62 | using (var xmlTrans = new XmlTransformation(HttpContext.Current.Server.MapPath(xdtFileName))) 63 | { 64 | if (xmlTrans.Apply(xmlDoc)) 65 | { 66 | // If we made it here, sourceDoc now has transDoc's changes 67 | // applied. So, we're going to save the final result off to 68 | // destDoc. 69 | try 70 | { 71 | xmlDoc.Save(HttpContext.Current.Server.MapPath(sourceDocFileName)); 72 | } 73 | catch (Exception e) 74 | { 75 | // Log error message 76 | var message = "Error executing TransformConfig package action (check file write permissions): " + e.Message; 77 | LogHelper.Error(typeof(TransformConfig), message, e); 78 | return false; 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | return true; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/AzureCdnToolkit.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit 2 | { 3 | using System.Web.Configuration; 4 | public sealed class AzureCdnToolkit 5 | { 6 | /// 7 | /// singleton instance 8 | /// 9 | private static readonly AzureCdnToolkit azureCdnToolkit = new AzureCdnToolkit(); 10 | 11 | /// 12 | /// Initialises static members of the class 13 | /// 14 | static AzureCdnToolkit() 15 | { 16 | } 17 | 18 | /// 19 | /// Prevents a default instance of the class from being created 20 | /// (singleton constructor) 21 | /// 22 | private AzureCdnToolkit() 23 | { 24 | this.Refresh(); 25 | } 26 | 27 | /// 28 | /// Gets the singleton instance of the class 29 | /// 30 | public static AzureCdnToolkit Instance 31 | { 32 | get 33 | { 34 | return azureCdnToolkit; 35 | } 36 | } 37 | 38 | /// 39 | /// Gets a value indicating whether the CDN prefix should be used 40 | /// 41 | public bool UseAzureCdnToolkit { get; set; } 42 | 43 | /// 44 | /// Gets or sets the version of the assets to use for client side cache busting 45 | /// 46 | public string CdnPackageVersion { get; set; } 47 | 48 | /// 49 | /// Gets the CDN Url 50 | /// 51 | public string CdnUrl { get; set; } 52 | 53 | /// 54 | /// Gets the Assets Container 55 | /// 56 | public string AssetsContainer { get; set; } 57 | 58 | /// 59 | /// Gets the Media Container 60 | /// 61 | public string MediaContainer { get; set; } 62 | 63 | /// 64 | /// Determines wqhether to use Redis Cache or Local Cache 65 | /// 66 | public bool UseRedisCache { get; set; } 67 | 68 | /// 69 | /// Determines wqhether to use Redis Cache or Local Cache 70 | /// 71 | public string RedisCacheConnectionString { get; set; } 72 | 73 | /// 74 | /// Sets all properties 75 | /// 76 | 77 | internal string Domain { get; set; } 78 | 79 | public void Refresh() 80 | { 81 | 82 | if (WebConfigurationManager.AppSettings["AzureCDNToolkit:UseAzureCdnToolkit"] != null) 83 | { 84 | var useAzureCdnToolkit = bool.Parse(WebConfigurationManager.AppSettings["AzureCDNToolkit:UseAzureCdnToolkit"]); 85 | this.UseAzureCdnToolkit = useAzureCdnToolkit; 86 | } 87 | else 88 | { 89 | this.UseAzureCdnToolkit = true; 90 | } 91 | 92 | this.Domain = WebConfigurationManager.AppSettings["AzureCDNToolkit:Domain"]; 93 | this.CdnPackageVersion = WebConfigurationManager.AppSettings["AzureCDNToolkit:CdnPackageVersion"]; 94 | this.CdnUrl = WebConfigurationManager.AppSettings["AzureCDNToolkit:CdnUrl"]; 95 | this.AssetsContainer = WebConfigurationManager.AppSettings["AzureCDNToolkit:AssetsContainer"] ?? "assets"; 96 | this.MediaContainer = WebConfigurationManager.AppSettings["AzureCDNToolkit:MediaContainer"] ?? "media"; 97 | this.UseRedisCache = WebConfigurationManager.AppSettings["AzureCDNToolkit:UseRedisCache"] != null && 98 | bool.Parse(WebConfigurationManager.AppSettings["AzureCDNToolkit:UseRedisCache"]); 99 | this.RedisCacheConnectionString = WebConfigurationManager.AppSettings["AzureCDNToolkit:RedisCacheConnectionString"]; 100 | } 101 | 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/Our.Umbraco.AzureCDNToolkit.Tests/RedisCacheTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using NUnit.Framework; 4 | 5 | namespace Our.Umbraco.AzureCDNToolkit.Tests 6 | { 7 | [TestFixture] 8 | public class RedisCacheTests 9 | { 10 | [Test] 11 | public void TestGetCacheItem() 12 | { 13 | AzureCdnToolkit.Instance.Refresh(); 14 | 15 | var stringToSet = "Test value"; 16 | var cacheKey = Guid.NewGuid().ToString(); 17 | 18 | // add one item to ensure something is there to flush 19 | RedisCache.InsertCacheItem(cacheKey, stringToSet); 20 | 21 | var cacheItem = RedisCache.GetCacheItem(cacheKey); 22 | 23 | Assert.IsNotNull(cacheItem); 24 | } 25 | 26 | [Test] 27 | public void TestInsertCacheItem() 28 | { 29 | AzureCdnToolkit.Instance.Refresh(); 30 | var endpoints = RedisCache.Connection.GetEndPoints(); 31 | var server = RedisCache.Connection.GetServer(endpoints.First()); 32 | var itemsBeforeInsert = server.DatabaseSize(); 33 | var cacheKey = Guid.NewGuid().ToString(); 34 | 35 | RedisCache.InsertCacheItem(cacheKey, "Test value"); 36 | 37 | var itemsAfterInsert = server.DatabaseSize(); 38 | 39 | Assert.AreNotEqual(itemsBeforeInsert, itemsAfterInsert); 40 | } 41 | 42 | [Test] 43 | public void TestGetCacheItemsByKeySearch() 44 | { 45 | AzureCdnToolkit.Instance.Refresh(); 46 | var cacheKey = Guid.NewGuid().ToString(); 47 | var cacheKeySub = cacheKey.Substring(0, 8); 48 | 49 | RedisCache.InsertCacheItem(cacheKey, "Test value"); 50 | 51 | var item = RedisCache.GetCacheItemsByKeySearch(cacheKeySub).FirstOrDefault(); 52 | 53 | Assert.IsNotNull(item); 54 | } 55 | 56 | [Test] 57 | public void TestClearCache() 58 | { 59 | AzureCdnToolkit.Instance.Refresh(); 60 | 61 | var endpoints = RedisCache.Connection.GetEndPoints(); 62 | var server = RedisCache.Connection.GetServer(endpoints.First()); 63 | 64 | // add one item to ensure something is there to flush 65 | RedisCache.Db.StringSet(Guid.NewGuid().ToString(), "Test value"); 66 | 67 | var keysBeforeFlush = server.DatabaseSize(); 68 | server.FlushAllDatabases(); 69 | var keysAfterFlush = server.DatabaseSize(); 70 | 71 | Assert.AreNotEqual(keysBeforeFlush, keysAfterFlush); 72 | } 73 | 74 | [Test] 75 | public void TestClearCacheItem() 76 | { 77 | AzureCdnToolkit.Instance.Refresh(); 78 | var cacheKey = Guid.NewGuid().ToString(); 79 | var stringToSet = "Test value"; 80 | 81 | RedisCache.InsertCacheItem(cacheKey, stringToSet); 82 | 83 | var insertedItem = RedisCache.GetCacheItem(cacheKey); 84 | 85 | Assert.AreEqual(insertedItem, stringToSet); 86 | 87 | RedisCache.ClearCacheItem(cacheKey); 88 | 89 | insertedItem = RedisCache.GetCacheItem(cacheKey); 90 | 91 | Assert.IsNull(insertedItem); 92 | } 93 | 94 | [Test] 95 | public void TestClearCacheByKeySearch() 96 | { 97 | AzureCdnToolkit.Instance.Refresh(); 98 | 99 | // insert some items that all start with the same string 100 | var cacheKey = Guid.NewGuid().ToString().Substring(0, 8); 101 | 102 | for (var i = 0; i < 10; i++) 103 | { 104 | var uniqueCacheKey = cacheKey + Guid.NewGuid(); 105 | RedisCache.InsertCacheItem(uniqueCacheKey, "Test value"); 106 | } 107 | 108 | var itemsBeforeDelete = RedisCache.GetCacheItemsByKeySearch(cacheKey); 109 | 110 | Assert.AreEqual(itemsBeforeDelete.Count(), 10); 111 | 112 | RedisCache.ClearCacheByKeySearch(cacheKey); 113 | 114 | var itemsAfterDelete = RedisCache.GetCacheItemsByKeySearch(cacheKey); 115 | 116 | Assert.AreNotEqual(itemsBeforeDelete.Count(), itemsAfterDelete.Count()); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /AzureCDNToolkit.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25123.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Our.Umbraco.AzureCDNToolkit", "src\Our.Umbraco.AzureCDNToolkit\Our.Umbraco.AzureCDNToolkit.csproj", "{0FD015D8-9060-4D5F-A722-E07B65A5EE35}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Our.Umbraco.AzureCDNToolkit.Tests", "tests\Our.Umbraco.AzureCDNToolkit.Tests\Our.Umbraco.AzureCDNToolkit.Tests.csproj", "{B5F8514E-6D0F-42C6-86E0-3AA278F31506}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{76969A1C-1C43-44D3-AB23-8223A981A6B6}" 11 | ProjectSection(SolutionItems) = preProject 12 | appveyor.yml = appveyor.yml 13 | build\AzureCDNToolkit.proj = build\AzureCDNToolkit.proj 14 | build-appveyor.cmd = build-appveyor.cmd 15 | build.cmd = build.cmd 16 | build\core-package.nuspec = build\core-package.nuspec 17 | build\package.nuspec = build\package.nuspec 18 | build\package.proj = build\package.proj 19 | build\package.xml = build\package.xml 20 | EndProjectSection 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Our.Umbraco.AzureCDNToolkit.Installer", "src\Our.Umbraco.AzureCDNToolkit.Installer\Our.Umbraco.AzureCDNToolkit.Installer.csproj", "{2587066D-369E-45BD-9DFD-028920570F73}" 23 | EndProject 24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Transforms", "Transforms", "{F7FA748D-43CA-44CC-9FE7-E94719EEA54C}" 25 | ProjectSection(SolutionItems) = preProject 26 | build\transforms\Dashboard.config.install.xdt = build\transforms\Dashboard.config.install.xdt 27 | build\transforms\Dashboard.config.uninstall.xdt = build\transforms\Dashboard.config.uninstall.xdt 28 | build\transforms\views.web.config.install.xdt = build\transforms\views.web.config.install.xdt 29 | build\transforms\views.web.config.uninstall.xdt = build\transforms\views.web.config.uninstall.xdt 30 | build\transforms\web.config.install.xdt = build\transforms\web.config.install.xdt 31 | build\transforms\web.config.uninstall.xdt = build\transforms\web.config.uninstall.xdt 32 | EndProjectSection 33 | EndProject 34 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Config", "Solution Config", "{F48814BB-8B91-4C47-922A-BF280E401731}" 35 | ProjectSection(SolutionItems) = preProject 36 | .editorconfig = .editorconfig 37 | .gitignore = .gitignore 38 | Rebracer.xml = Rebracer.xml 39 | EndProjectSection 40 | EndProject 41 | Global 42 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 43 | Debug|Any CPU = Debug|Any CPU 44 | Release|Any CPU = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 47 | {8258CC24-C75A-4498-B1D9-EF4CA543706F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {8258CC24-C75A-4498-B1D9-EF4CA543706F}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {8258CC24-C75A-4498-B1D9-EF4CA543706F}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {8258CC24-C75A-4498-B1D9-EF4CA543706F}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {0FD015D8-9060-4D5F-A722-E07B65A5EE35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {0FD015D8-9060-4D5F-A722-E07B65A5EE35}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {0FD015D8-9060-4D5F-A722-E07B65A5EE35}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {0FD015D8-9060-4D5F-A722-E07B65A5EE35}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {B5F8514E-6D0F-42C6-86E0-3AA278F31506}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {B5F8514E-6D0F-42C6-86E0-3AA278F31506}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {B5F8514E-6D0F-42C6-86E0-3AA278F31506}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {B5F8514E-6D0F-42C6-86E0-3AA278F31506}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {2587066D-369E-45BD-9DFD-028920570F73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {2587066D-369E-45BD-9DFD-028920570F73}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {2587066D-369E-45BD-9DFD-028920570F73}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {2587066D-369E-45BD-9DFD-028920570F73}.Release|Any CPU.Build.0 = Release|Any CPU 63 | EndGlobalSection 64 | GlobalSection(SolutionProperties) = preSolution 65 | HideSolutionNode = FALSE 66 | EndGlobalSection 67 | GlobalSection(NestedProjects) = preSolution 68 | {F7FA748D-43CA-44CC-9FE7-E94719EEA54C} = {76969A1C-1C43-44D3-AB23-8223A981A6B6} 69 | EndGlobalSection 70 | EndGlobal 71 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/App_Plugins/AzureCDNToolkit/ImagePathCacheDashboard.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('umbraco') 3 | 4 | .controller('AzureCDNToolKit.CacheController', ['$scope', '$http', function ($scope, $http) { 5 | 6 | // Get WebApi urls 7 | var cacheApiBaseUrl = Umbraco.Sys.ServerVariables.azureCdnToolkitUrls.cacheApiBaseUrl; 8 | var sendCachedImagesRequestApiUrl = cacheApiBaseUrl + "SendCachedImagesRequest"; 9 | var getAllCachedImagesFromRequestApiUrl = cacheApiBaseUrl + "GetAllCachedImagesFromRequest"; 10 | var wipeApiUrl = cacheApiBaseUrl + "Wipe"; 11 | var getAllServersApiUrl = cacheApiBaseUrl + "GetAllServers"; 12 | 13 | $scope.statuses = []; 14 | $scope.waiting = false; 15 | 16 | $scope.getAllCachedImagesForServer = function (server) { 17 | $scope.statuses = []; 18 | $scope.success = false; 19 | $http({ 20 | method: 'POST', 21 | url: sendCachedImagesRequestApiUrl, 22 | params: { 23 | 'serverIdentity': server 24 | }, 25 | data: {} 26 | }) 27 | .success(function (data, status, headers, config) { 28 | var requestId; 29 | if (typeof data === "string") { 30 | requestId = JSON.parse(data); 31 | } 32 | else { 33 | requestId = data; 34 | } 35 | $scope.requestId = requestId; 36 | }) 37 | .then(function () { 38 | 39 | var getResponse = function () { 40 | 41 | $scope.waiting = true; 42 | 43 | // do something with the request id to get the data we actually want 44 | $http({ 45 | method: 'POST', 46 | url: getAllCachedImagesFromRequestApiUrl, 47 | params: { 48 | 'requestId': $scope.requestId 49 | }, 50 | data: {} 51 | }).success(function (data, status, headers, config) { 52 | 53 | if (angular.isArray(data)) { 54 | $scope.statuses = data; 55 | $scope.success = true; 56 | $scope.waiting = false; 57 | } else { 58 | // try again 59 | getResponse(); 60 | } 61 | }); 62 | }; 63 | 64 | getResponse(); 65 | 66 | }); 67 | }; 68 | 69 | $scope.rowClass = function (image) { 70 | if (image.resolved === false) { 71 | return "warning"; 72 | } 73 | if (image.weburl === "" || image.cacheurl === "") { 74 | return "error"; 75 | } 76 | if (image.weburl !== image.cacheurl) { 77 | return "success"; 78 | } 79 | return "warning"; 80 | } 81 | 82 | $scope.wipe = function (server, weburl) { 83 | $http({ 84 | method: 'POST', 85 | url: wipeApiUrl, 86 | params: { 87 | 'serverIdentity': server, 88 | 'weburl': weburl 89 | }, 90 | data: {} 91 | }).success(function () { 92 | // clear all triggered 93 | if (typeof (weburl) == "undefined") { 94 | $scope.statuses = []; 95 | } else { 96 | // remove from view 97 | $scope.statuses = $scope.statuses.filter(function (value) { return value.weburl !== weburl; }); 98 | } 99 | }); 100 | }; 101 | 102 | $scope.getAllServers = function () { 103 | 104 | var getResponse = function() { 105 | $http.get(getAllServersApiUrl) 106 | .success(function (data) { 107 | if (angular.isArray(data)) { 108 | $scope.servers = data; 109 | $scope.selectedserver = $scope.servers[0]; 110 | } else { 111 | // try again 112 | getResponse(); 113 | } 114 | }); 115 | }; 116 | 117 | getResponse(); 118 | 119 | }; 120 | 121 | $scope.getAllServers(); 122 | 123 | }]); -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Controllers/CacheApiController.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.Controllers 2 | { 3 | using System.Linq; 4 | using System.Threading; 5 | using System; 6 | using System.Web.Http; 7 | using System.Collections.Generic; 8 | 9 | using global::Umbraco.Web.Cache; 10 | 11 | using global::Umbraco.Web.Mvc; 12 | using global::Umbraco.Web.WebApi; 13 | 14 | using Newtonsoft.Json; 15 | 16 | using CacheRefreshers; 17 | using Models; 18 | 19 | [PluginController("AzureCDNToolkit")] 20 | public class CacheApiController : UmbracoAuthorizedApiController 21 | { 22 | /// 23 | /// Sends a cache request message specifying a particular server retrun cache stats 24 | /// 25 | /// ~/Umbraco/backoffice/AzureCDNToolkit/CacheApi/SendCachedImagesRequest 26 | [HttpPost] 27 | [global::Umbraco.Web.WebApi.UmbracoAuthorize] 28 | public Guid SendCachedImagesRequest(string serverIdentity) 29 | { 30 | var thisRequestId = Guid.NewGuid(); 31 | 32 | var thisRequest = new CachedImagesRequest() 33 | { 34 | RequestId = thisRequestId, 35 | ServerIdentity = serverIdentity 36 | }; 37 | 38 | var json = JsonConvert.SerializeObject(thisRequest); 39 | 40 | DistributedCache.Instance.RefreshByJson(CacheRequester.Guid, json); 41 | return thisRequestId; 42 | } 43 | 44 | /// 45 | /// Gets all cache image urls 46 | /// 47 | /// object 48 | /// ~/Umbraco/backoffice/AzureCDNToolkit/CacheApi/GetAllCachedImagesFromRequest 49 | [HttpPost] 50 | [global::Umbraco.Web.WebApi.UmbracoAuthorize] 51 | public IEnumerable GetAllCachedImagesFromRequest(string requestId) 52 | { 53 | var cacheKey = string.Format("{0}{1}", AzureCDNToolkit.Constants.Keys.CachePrefixResponse, requestId); 54 | 55 | // it can take time for servers to return the data so try 6 times waiting 10 seconds between each try 56 | for (int retry = 0;; retry++) 57 | { 58 | var cachedResponse = Cache.GetCacheItem>(cacheKey); 59 | 60 | if (cachedResponse != null) 61 | { 62 | return cachedResponse; 63 | } 64 | 65 | Thread.Sleep(TimeSpan.FromSeconds(10)); 66 | 67 | if (retry >= 6) 68 | { 69 | break; 70 | } 71 | } 72 | return null; 73 | } 74 | 75 | /// 76 | /// Wipes the image cache urls 77 | /// 78 | /// ~/Umbraco/backoffice/AzureCDNToolkit/CacheApi/WipeAll 79 | [HttpPost] 80 | [global::Umbraco.Web.WebApi.UmbracoAuthorize] 81 | public void Wipe(string serverIdentity, string webUrl = null) 82 | { 83 | var thisRequest = new CachedImagesWipe() 84 | { 85 | ServerIdentity = serverIdentity, 86 | WebUrl = webUrl 87 | }; 88 | 89 | var json = JsonConvert.SerializeObject(thisRequest); 90 | 91 | DistributedCache.Instance.RefreshByJson(CacheWiper.Guid, json); 92 | } 93 | 94 | /// 95 | /// Gets a collection of all servers from the ServerRegistrationService 96 | /// 97 | /// ~/Umbraco/backoffice/AzureCDNToolkit/CacheApi/GetAllServers 98 | public string[] GetAllServers() 99 | { 100 | // will try for 5 times waiting 3 seconds to get a list of servers as they can take time to register mainly when developing locally 101 | for (int retry = 0; ; retry++) 102 | { 103 | var serverDetails = ApplicationContext.Services.ServerRegistrationService.GetActiveServers(); 104 | if (serverDetails.Any()) 105 | { 106 | return serverDetails.Select(server => server.ServerIdentity).ToArray(); 107 | } 108 | 109 | Thread.Sleep(TimeSpan.FromSeconds(3)); 110 | 111 | if (retry >= 5) 112 | { 113 | break; 114 | } 115 | } 116 | 117 | return null; 118 | } 119 | 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This package is retired and will not be updated for Umbraco v8+ # 2 | **For Umbraco v8 Azure CDN integration use Slimsy v3** - documentation at https://github.com/Jeavon/Slimsy/blob/dev-v3/Docs/Azure-CDN/index.md 3 | 4 | # Umbraco Azure CDN Toolkit # 5 | 6 | ![Azure Logger](build/assets/icon/umbraco-azure-toolkit-256.png) 7 | 8 | The AzureCDNToolkit package allows you to fully utilise and integrate the Azure CDN with your Umbraco powered website using a "Origin Push" approach. 9 | 10 | **To Push or to Pull** 11 | You can use both a Push or a Pull origin approach to the Azure CDN with Umbraco since the release of the new Azure CDN (AzureEdge), this package supports only Push origin (storage) currently however it maybe extended to also support Pull in the future. 12 | 13 | There are three file types that should be served from CDN if you have one. 14 | 15 | - Assets - css, js & static images used by templates etc.. 16 | - Images managed by Umbraco - cropped or not 17 | - Files - pdfs, docx etc 18 | 19 | The toolkit depends on two packages being installed, these are the [UmbracoFileSystemProviders.Azure](https://github.com/JimBobSquarePants/UmbracoFileSystemProviders.Azure) and the [ImageProcessor.Web Azure Blob Cache plugin](http://imageprocessor.org/imageprocessor-web/plugins/azure-blob-cache/) 20 | 21 | Once installed and setup the package provides UrlHelper methods to use for resolving url paths for assets and Image Cropper urls and also a value converter for the TinyMce editor so that images within the content are also resolved. 22 | 23 | Some examples: 24 | 25 | @Url.ResolveCdn("/css/style.css") 26 | 27 | @Url.GetCropCdnUrl(Umbraco.TypedMedia(1084), width: 150) 28 | 29 |
30 | 31 | When using these methods, the toolkit will attempt to resolve the urls to their **absolute** paths and **crucially** ensures that a cache busting querystring variable is added. Without the cache busting using the Azure CDN can become tricky when you want to update with new content. This has the added benefit of avoiding your site needing to handle any 301 redirects and also gets some optimisation benefit (PageSpeed etc). 32 | 33 | Some examples: 34 | 35 | 1. `` 36 | 2. `` 37 | 3. `Download Word Doc` 38 | 39 | Becomes: 40 | 41 | 1. `` 42 | 2. `` 43 | 3. `Download Word Doc` 44 | 45 | [![Build status](https://ci.appveyor.com/api/projects/status/7lj6r6uoofm9mb24?svg=true)](https://ci.appveyor.com/project/JeavonLeopold/umbraco-azurecdntoolkit) 46 | 47 | **Note**: this package is not compatible with [Slimsy](https://github.com/Jeavon/Slimsy) v1 (it should work with the upcoming Slimsy v2 using img srcset) 48 | 49 | ## Documentation ## 50 | 51 | 1. [Azure Setup](docs/Azure-Setup.md) 52 | 2. [Umbraco Setup](docs/Umbraco-Setup.md) 53 | 3. [Umbraco Implementation](docs/Umbraco-Implementation.md) 54 | 4. [Umbraco Dashboard](docs/Umbraco-Dashboard.md) 55 | 5. [Reference](docs/Reference.md) 56 | 57 | ## Installation ## 58 | 59 | Both NuGet and Umbraco packages are available. 60 | 61 | |NuGet Packages |Version | 62 | |:-----------------|:-----------------| 63 | |**Release**|[![NuGet download](http://img.shields.io/nuget/v/Our.Umbraco.AzureCDNToolkit.svg)](https://www.nuget.org/packages/Our.Umbraco.AzureCDNToolkit/) 64 | |**Pre-release**|[![MyGet download](https://img.shields.io/myget/umbraco-packages/vpre/Our.Umbraco.AzureCDNToolkit.svg)](https://www.myget.org/feed/umbraco-packages/package/nuget/Our.Umbraco.AzureCDNToolkit) 65 | 66 | |Umbraco Packages | | 67 | |:-----------------|:-----------------| 68 | |**Release**|[![Our Umbraco project page](https://img.shields.io/badge/our-umbraco-orange.svg)](https://our.umbraco.org/projects/developer-tools/azure-cdn-toolkit-for-umbraco/) 69 | |**Pre-release**| [![AppVeyor Artifacts](https://img.shields.io/badge/appveyor-umbraco-orange.svg)](https://ci.appveyor.com/project/JeavonLeopold/umbraco-azurecdntoolkit/build/artifacts) 70 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Events/CacheEvents.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.Events 2 | { 3 | using System.Collections.Generic; 4 | 5 | using Newtonsoft.Json; 6 | 7 | using global::Umbraco.Core; 8 | using global::Umbraco.Core.Cache; 9 | using global::Umbraco.Web.Cache; 10 | using global::Umbraco.Core.Logging; 11 | 12 | using CacheRefreshers; 13 | using Models; 14 | 15 | public class CacheEvents: ApplicationEventHandler 16 | { 17 | protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, 18 | ApplicationContext applicationContext) 19 | { 20 | CacheRefresherBase.CacheUpdated += CacheRequester_Request; 21 | CacheRefresherBase.CacheUpdated += CacheResponder_Response; 22 | CacheRefresherBase.CacheUpdated += CacheWiper_Request; 23 | } 24 | 25 | /// 26 | /// Handles all cache 'requests', and checks to see if the current machine should respond (with another 'cache refresher') 27 | /// 28 | /// 29 | /// 30 | private void CacheRequester_Request(CacheRequester sender, CacheRefresherEventArgs e) 31 | { 32 | var rawPayLoad = (string)e.MessageObject; 33 | 34 | var payload = JsonConvert.DeserializeObject(rawPayLoad); 35 | 36 | if ( 37 | ApplicationContext.Current.Services.ServerRegistrationService.CurrentServerIdentity.InvariantEquals( 38 | payload.ServerIdentity)) 39 | { 40 | // THIS SERVER SHOULD RETURN DATA VIA CacheImagesResponder 41 | 42 | var cachedItems = Cache.GetCacheItemsByKeySearch(AzureCDNToolkit.Constants.Keys.CachePrefix); 43 | 44 | var response = new CachedImagesResponse() 45 | { 46 | RequestId = payload.RequestId, 47 | CachedImages = cachedItems 48 | }; 49 | 50 | var json = JsonConvert.SerializeObject(response); 51 | DistributedCache.Instance.RefreshByJson(CacheResponder.Guid, json); 52 | } 53 | } 54 | 55 | /// 56 | /// 57 | /// 58 | /// 59 | /// 60 | private void CacheResponder_Response(CacheResponder sender, CacheRefresherEventArgs e) 61 | { 62 | var rawPayLoad = (string)e.MessageObject; 63 | var payload = JsonConvert.DeserializeObject(rawPayLoad); 64 | 65 | var cacheKey = string.Format("{0}{1}", AzureCDNToolkit.Constants.Keys.CachePrefixResponse, payload.RequestId); 66 | Cache.InsertCacheItem>(cacheKey, () => payload.CachedImages); 67 | } 68 | 69 | 70 | /// 71 | /// Handles all cache 'requests', and checks to see if the current machine should respond (with another 'cache refresher') 72 | /// 73 | /// 74 | /// 75 | private void CacheWiper_Request(CacheWiper sender, CacheRefresherEventArgs e) 76 | { 77 | var rawPayLoad = (string)e.MessageObject; 78 | 79 | var payload = JsonConvert.DeserializeObject(rawPayLoad); 80 | 81 | if ( 82 | ApplicationContext.Current.Services.ServerRegistrationService.CurrentServerIdentity.InvariantEquals( 83 | payload.ServerIdentity)) 84 | { 85 | // This server should wipe it's application cache 86 | 87 | if (payload.WebUrl != null) 88 | { 89 | // wipe specific url 90 | var cachePrefix = AzureCDNToolkit.Constants.Keys.CachePrefix; 91 | var cacheKey = string.Format("{0}{1}", cachePrefix, payload.WebUrl); 92 | Cache.ClearCacheItem(cacheKey); 93 | 94 | LogHelper.Info(string.Format("Azure CDN Toolkit: CDN image path runtime cache for key {0} cleared by dashboard control request", payload.WebUrl)); 95 | } 96 | else 97 | { 98 | // clear all keys 99 | Cache.ClearCacheByKeySearch(AzureCDNToolkit.Constants.Keys.CachePrefix); 100 | 101 | LogHelper.Info("Azure CDN Toolkit: CDN image path runtime cache cleared by dashboard control request"); 102 | } 103 | 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/ImageCropperBaseExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Globalization; 6 | using System.Linq; 7 | using System.Text; 8 | using Newtonsoft.Json; 9 | using global::Umbraco.Core.Logging; 10 | using global::Umbraco.Web.Models; 11 | internal static class ImageCropperBaseExtensions 12 | { 13 | internal static ImageCropData GetImageCrop(this string json, string id) 14 | { 15 | var ic = new ImageCropData(); 16 | if (json.DetectIsJson()) 17 | { 18 | try 19 | { 20 | var imageCropperSettings = JsonConvert.DeserializeObject>(json); 21 | ic = imageCropperSettings.GetCrop(id); 22 | } 23 | catch (Exception ex) 24 | { 25 | LogHelper.Error(typeof(ImageCropperBaseExtensions), "Could not parse the json string: " + json, ex); 26 | } 27 | } 28 | 29 | return ic; 30 | } 31 | 32 | internal static ImageCropDataSet SerializeToCropDataSet(this string json) 33 | { 34 | var imageCrops = new ImageCropDataSet(); 35 | if (json.DetectIsJson()) 36 | { 37 | try 38 | { 39 | imageCrops = JsonConvert.DeserializeObject(json); 40 | } 41 | catch (Exception ex) 42 | { 43 | LogHelper.Error(typeof(ImageCropperBaseExtensions), "Could not parse the json string: " + json, ex); 44 | } 45 | } 46 | 47 | return imageCrops; 48 | } 49 | 50 | internal static ImageCropData GetCrop(this ImageCropDataSet dataset, string cropAlias) 51 | { 52 | if (dataset == null || dataset.Crops == null || !dataset.Crops.Any()) 53 | return null; 54 | 55 | return dataset.Crops.GetCrop(cropAlias); 56 | } 57 | 58 | internal static ImageCropData GetCrop(this IEnumerable dataset, string cropAlias) 59 | { 60 | if (dataset == null || !dataset.Any()) 61 | return null; 62 | 63 | if (string.IsNullOrEmpty(cropAlias)) 64 | return dataset.FirstOrDefault(); 65 | 66 | return dataset.FirstOrDefault(x => x.Alias.ToLowerInvariant() == cropAlias.ToLowerInvariant()); 67 | } 68 | 69 | internal static string GetCropBaseUrl(this ImageCropDataSet cropDataSet, string cropAlias, bool preferFocalPoint) 70 | { 71 | var cropUrl = new StringBuilder(); 72 | 73 | var crop = cropDataSet.GetCrop(cropAlias); 74 | 75 | // if crop alias has been specified but not found in the Json we should return null 76 | if (string.IsNullOrEmpty(cropAlias) == false && crop == null) 77 | { 78 | return null; 79 | } 80 | if ((preferFocalPoint && cropDataSet.HasFocalPoint()) || (crop != null && crop.Coordinates == null && cropDataSet.HasFocalPoint()) || (string.IsNullOrEmpty(cropAlias) && cropDataSet.HasFocalPoint())) 81 | { 82 | cropUrl.Append("?center=" + cropDataSet.FocalPoint.Top.ToString(CultureInfo.InvariantCulture) + "," + cropDataSet.FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); 83 | cropUrl.Append("&mode=crop"); 84 | } 85 | else if (crop != null && crop.Coordinates != null && preferFocalPoint == false) 86 | { 87 | cropUrl.Append("?crop="); 88 | cropUrl.Append(crop.Coordinates.X1.ToString(CultureInfo.InvariantCulture)).Append(","); 89 | cropUrl.Append(crop.Coordinates.Y1.ToString(CultureInfo.InvariantCulture)).Append(","); 90 | cropUrl.Append(crop.Coordinates.X2.ToString(CultureInfo.InvariantCulture)).Append(","); 91 | cropUrl.Append(crop.Coordinates.Y2.ToString(CultureInfo.InvariantCulture)); 92 | cropUrl.Append("&cropmode=percentage"); 93 | } 94 | else 95 | { 96 | cropUrl.Append("?anchor=center"); 97 | cropUrl.Append("&mode=crop"); 98 | } 99 | return cropUrl.ToString(); 100 | } 101 | 102 | /// 103 | /// This tries to detect a json string, this is not a fail safe way but it is quicker than doing 104 | /// a try/catch when deserializing when it is not json. 105 | /// 106 | /// 107 | /// 108 | internal static bool DetectIsJson(this string input) 109 | { 110 | input = input.Trim(); 111 | return (input.StartsWith("{") && input.EndsWith("}")) 112 | || (input.StartsWith("[") && input.EndsWith("]")); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/ValueConverters/RteValueConverter.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit.ValueConverters 2 | { 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Mvc; 6 | using System.Collections.Specialized; 7 | 8 | using global::Umbraco.Core; 9 | using global::Umbraco.Web; 10 | using global::Umbraco.Core.Models.PublishedContent; 11 | using global::Umbraco.Core.PropertyEditors; 12 | using global::Umbraco.Web.PropertyEditors.ValueConverters; 13 | 14 | using HtmlAgilityPack; 15 | 16 | [PropertyValueType(typeof(IHtmlString))] 17 | [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] 18 | public class RteValueConverter : RteMacroRenderingValueConverter 19 | { 20 | public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) 21 | { 22 | if (source == null) 23 | { 24 | return null; 25 | } 26 | 27 | var coreConversion = base.ConvertDataToSource( 28 | propertyType, 29 | source, 30 | preview); 31 | 32 | // If toolkit is disabled then return base conversion 33 | if (!AzureCdnToolkit.Instance.UseAzureCdnToolkit) 34 | { 35 | return coreConversion; 36 | } 37 | 38 | var doc = new HtmlDocument(); 39 | doc.LoadHtml(coreConversion.ToString()); 40 | 41 | if (doc.ParseErrors.Any() || doc.DocumentNode == null) 42 | { 43 | return coreConversion; 44 | } 45 | 46 | var modified = false; 47 | 48 | ResolveUrlsForElement(doc, "img", "src", "data-id", false, false, ref modified); 49 | ResolveUrlsForElement(doc, "a", "href", "data-id", true, true, ref modified); 50 | 51 | return modified ? doc.DocumentNode.OuterHtml : coreConversion; 52 | } 53 | 54 | private static void ResolveUrlsForElement(HtmlDocument doc, string elementName, string attributeName, string idAttributeName, bool idAttributeMandatory, bool asset, ref bool modified) 55 | { 56 | var htmlNodes = doc.DocumentNode.SelectNodes(string.Concat("//", elementName)); 57 | 58 | if (htmlNodes == null) 59 | { 60 | return; 61 | } 62 | 63 | foreach (var htmlNode in htmlNodes) 64 | { 65 | var urlAttr = htmlNode.Attributes.FirstOrDefault(x => x.Name == attributeName); 66 | var idAttr = htmlNode.Attributes.FirstOrDefault(x => x.Name == idAttributeName); 67 | 68 | if (urlAttr == null || (idAttributeMandatory && idAttr == null)) 69 | { 70 | continue; 71 | } 72 | 73 | // html decode the url as variables encoded in tinymce 74 | var src = HttpUtility.HtmlDecode(urlAttr.Value); 75 | var resolvedSrc = string.Empty; 76 | 77 | var hasQueryString = src.InvariantContains("?"); 78 | var querystring = new NameValueCollection(); 79 | 80 | if (hasQueryString && src != null) 81 | { 82 | querystring = HttpUtility.ParseQueryString(src.Substring(src.IndexOf('?'))); 83 | } 84 | 85 | 86 | // can only resolve ImageProcessor Azure Cache Urls if resolvable domain is set 87 | if (AzureCdnToolkit.Instance.Domain == null) 88 | { 89 | continue; 90 | } 91 | if (idAttr != null) 92 | { 93 | // Umbraco media 94 | int nodeId; 95 | if (int.TryParse(idAttr.Value, out nodeId)) 96 | { 97 | var node = UmbracoContext.Current.MediaCache.GetById(nodeId); 98 | 99 | if (node != null) 100 | { 101 | if (hasQueryString) 102 | { 103 | resolvedSrc = 104 | new UrlHelper().ResolveCdnFallback(node, asset: asset, 105 | querystring: querystring.ToString(), fallbackImage: src).ToString(); 106 | } 107 | else 108 | { 109 | resolvedSrc = 110 | new UrlHelper().ResolveCdnFallback(node, asset: asset, fallbackImage: src) 111 | .ToString(); 112 | } 113 | } 114 | } 115 | } 116 | else 117 | { 118 | // Image in TinyMce doesn't have a data-id attribute so lets add package cache buster 119 | resolvedSrc = new UrlHelper().ResolveCdn(src, asset: asset).ToString(); 120 | } 121 | 122 | // If the resolved url is different to the orginal change the src attribute 123 | if (string.IsNullOrWhiteSpace(resolvedSrc) || resolvedSrc == string.Concat(AzureCdnToolkit.Instance.Domain, src)) 124 | { 125 | continue; 126 | } 127 | 128 | urlAttr.Value = resolvedSrc; 129 | modified = true; 130 | } 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /docs/Umbraco-Setup.md: -------------------------------------------------------------------------------- 1 | # Umbraco Setup # 2 | 3 | ## Prerequisites ## 4 | 5 | You will need to have a Visual Studio Web Application project with Umbraco v7.3.8 or above installed. 6 | 7 | Ensure your content managed image type media items are using a Image Cropper for the `umbracoFile` property (this is default in Umbraco v7.4+) 8 | 9 | ![Image Type Cropper](images/image-type-cropper.png) 10 | 11 | ## 1. ImageProcessor.Web.Config ## 12 | 13 | Install this package via NuGet, it will add three configuration files to your solution. 14 | 15 | NuGet Package: 16 | 17 | Install-Package ImageProcessor.Web.Config 18 | 19 | ## 2. UmbracoFileSystemProviders.Azure ## 20 | 21 | Use v0.5.2-beta or above 22 | 23 | Install the UmbracoFileSystemProviders.Azure package. Recommended that you first install using the Umbraco package and then install the NuGet package over the top, configuration will be maintained, this is to take advantage of the Umbraco installer configuring everything needed automatically. 24 | 25 | [Download Umbraco package](https://our.umbraco.org/projects/collaboration/umbracofilesystemprovidersazure/) 26 | 27 | NuGet Package: 28 | 29 | Install-Package UmbracoFileSystemProviders.Azure -Pre 30 | 31 | When running the configuration in the Umbraco installer be sure to use the key and account name from [Step 2 of the Azure Setup.](Azure-Setup.md) 32 | 33 | ![Installing Umbraco File System Providers](images/umbraco-azure-filesystem-provider-install.png) 34 | 35 | **Note:** Be sure to leave the "UseDefaultRoute" checked 36 | 37 | ## 3. ImageProcessor.Web.Plugins.AzureBlobCache 38 | 39 | Install this package via NuGet 40 | 41 | NuGet Package: 42 | 43 | Install-Package ImageProcessor.Web.Plugins.AzureBlobCache 44 | 45 | Once installed you will need to edit /Config/imageprocessor/cache.config 46 | 47 | It should look like this: 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | In the AzureBlobCache section you need to update the following keys: 72 | 73 | 1. CachedStorageAccount set to the key and account name from [Step 2 of the Azure Setup](Azure-Setup.md). 74 | 2. CachedBlobContainer change this if you want to (e.g. cloudcache), it will be included in the path of cropped images. 75 | 3. CachedCDNRoot set to the name of your CDN endpoint from [Step 5 of the Azure Setup](Azure-Setup.md). 76 | 4. It's recommended you change the CachedCDNTimeout to 2000 77 | 78 | Now change: 79 | 80 | 81 | 82 | To 83 | 84 | 85 | 86 | This will enable ImageProcessor.Web to store it's cache files in your storage account container. 87 | 88 | ## 4. Our.Umbraco.AzureCDNToolkit ## 89 | 90 | NuGet Package: 91 | 92 | Install-Package Our.Umbraco.AzureCDNToolkit -Pre 93 | 94 | Once installed you will find 6 new keys in web.config 95 | 96 | The `UseAzureCdnToolkit` turns CDN Toolkit on or off, generally when developing on a local machine you will want this set to false so you can easily update assets but set to true on stage/live servers. This setting doesn't have any effect on enabling disabling the dependant packages. 97 | 98 | 99 | 100 | The `Domain` key has to be set the url of the website being served from Umbraco. This url must be resolvable by the server itself, so watch out for servers that don't have dns loopback. If load balancing it doesn't matter if this url resolves from one server to another. 101 | 102 | 103 | 104 | The `CdnUrl` set to the name of your CDN endpoint from [Step 5 of the Azure Setup](Azure-Setup.md). 105 | 106 | 107 | 108 | The `CdnPackageVersion` is used to cache bust assets on your CDN 109 | 110 | 111 | 112 | The `AssetsContainer` allows a different name for your assets container if you wish to (e.g. you may have multiple sites in the same account) 113 | 114 | 115 | 116 | The `MediaContainer` allows a different name for your media container if you wish to (e.g. you may have multiple sites in the same account) 117 | 118 | 119 | 120 | When `SecurityModeEnabled` is enabled, the CDN toolkit will lock down ImageProcessor so that it will not process requests made directly via querystrings in the URL. This is to prevent any sort of ddos attack which could otherwise flood the CDN. This setting was added in **v0.2.0-beta** 121 | 122 | 123 | 124 | The value of this key must be valid in a querystring. e.g "unicornsrock123456789". Please be aware that all requests to ImageProcessor must be executed through the methods of this toolkit when this setting is enabled. This setting was added in **v0.2.0-beta** 125 | 126 | 127 | 128 | ## 5. Upload assets to the "assets" container ## 129 | 130 | Upload your static assets to the container created in [Step 7 of the Azure Setup](Azure-Setup.md). 131 | 132 | ![Upload assets](images/upload-assets.png) 133 | 134 | ## 6. Check everything works ## 135 | 136 | Before proceeding to [implementing](Umbraco-Implementation.md) the AzureCDNToolkit it's worth checking that the installed packages are working correctly and Azure is setup and ready. 137 | 138 | Checklist: 139 | 140 | - Check images load on the front end and in the Umbraco media section in a cropper 141 | - Check file media downloads ok 142 | 143 | If you get 404 errors on the front end, it's likely that your CDN endpoint hasn't had long enough to setup. -------------------------------------------------------------------------------- /tests/Our.Umbraco.AzureCDNToolkit.Tests/Our.Umbraco.AzureCDNToolkit.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {B5F8514E-6D0F-42C6-86E0-3AA278F31506} 8 | Library 9 | Properties 10 | Our.Umbraco.AzureCDNToolkit.Tests 11 | Our.Umbraco.AzureCDNToolkit.Tests 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | ..\..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.dll 35 | True 36 | 37 | 38 | ..\..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll 39 | True 40 | 41 | 42 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\businesslogic.dll 43 | True 44 | 45 | 46 | ..\..\packages\ClientDependency.1.8.4\lib\net45\ClientDependency.Core.dll 47 | True 48 | 49 | 50 | ..\..\packages\ClientDependency-Mvc5.1.8.0.0\lib\net45\ClientDependency.Core.Mvc.dll 51 | True 52 | 53 | 54 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\cms.dll 55 | True 56 | 57 | 58 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\controls.dll 59 | True 60 | 61 | 62 | ..\..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll 63 | True 64 | 65 | 66 | ..\..\packages\Examine.0.1.68.0\lib\Examine.dll 67 | True 68 | 69 | 70 | ..\..\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll 71 | True 72 | 73 | 74 | ..\..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll 75 | True 76 | 77 | 78 | ..\..\packages\ImageProcessor.2.5.1\lib\net45\ImageProcessor.dll 79 | True 80 | 81 | 82 | ..\..\packages\ImageProcessor.Web.4.8.0\lib\net45\ImageProcessor.Web.dll 83 | True 84 | 85 | 86 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\interfaces.dll 87 | True 88 | 89 | 90 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\log4net.dll 91 | True 92 | 93 | 94 | ..\..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll 95 | True 96 | 97 | 98 | ..\..\packages\Markdown.1.14.4\lib\net45\MarkdownSharp.dll 99 | True 100 | 101 | 102 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\Microsoft.ApplicationBlocks.Data.dll 103 | True 104 | 105 | 106 | ..\..\packages\Microsoft.AspNet.Identity.Core.2.2.1\lib\net45\Microsoft.AspNet.Identity.Core.dll 107 | True 108 | 109 | 110 | ..\..\packages\Microsoft.AspNet.Identity.Owin.2.2.1\lib\net45\Microsoft.AspNet.Identity.Owin.dll 111 | True 112 | 113 | 114 | ..\..\packages\Microsoft.IO.RecyclableMemoryStream.1.2.0\lib\net45\Microsoft.IO.RecyclableMemoryStream.dll 115 | True 116 | 117 | 118 | ..\..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll 119 | True 120 | 121 | 122 | ..\..\packages\Microsoft.Owin.Host.SystemWeb.3.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll 123 | True 124 | 125 | 126 | ..\..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll 127 | True 128 | 129 | 130 | ..\..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll 131 | True 132 | 133 | 134 | ..\..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll 135 | True 136 | 137 | 138 | ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll 139 | True 140 | 141 | 142 | ..\..\packages\MiniProfiler.2.1.0\lib\net40\MiniProfiler.dll 143 | True 144 | 145 | 146 | ..\..\packages\MySql.Data.6.9.8\lib\net45\MySql.Data.dll 147 | True 148 | 149 | 150 | ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll 151 | True 152 | 153 | 154 | ..\..\packages\NUnit.3.2.1\lib\net45\nunit.framework.dll 155 | True 156 | 157 | 158 | ..\..\packages\Owin.1.0\lib\net40\Owin.dll 159 | True 160 | 161 | 162 | ..\..\packages\semver.1.1.2\lib\net45\Semver.dll 163 | True 164 | 165 | 166 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\SQLCE4Umbraco.dll 167 | True 168 | 169 | 170 | ..\..\packages\StackExchange.Redis.1.2.6\lib\net45\StackExchange.Redis.dll 171 | 172 | 173 | 174 | 175 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\System.Data.SqlServerCe.dll 176 | True 177 | 178 | 179 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\System.Data.SqlServerCe.Entity.dll 180 | True 181 | 182 | 183 | 184 | ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll 185 | True 186 | 187 | 188 | 189 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll 190 | True 191 | 192 | 193 | ..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll 194 | True 195 | 196 | 197 | ..\..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll 198 | True 199 | 200 | 201 | ..\..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll 202 | True 203 | 204 | 205 | ..\..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll 206 | True 207 | 208 | 209 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll 210 | True 211 | 212 | 213 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll 214 | True 215 | 216 | 217 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll 218 | True 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\TidyNet.dll 228 | True 229 | 230 | 231 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\umbraco.dll 232 | True 233 | 234 | 235 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\Umbraco.Core.dll 236 | True 237 | 238 | 239 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\umbraco.DataLayer.dll 240 | True 241 | 242 | 243 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\umbraco.editorControls.dll 244 | True 245 | 246 | 247 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\umbraco.MacroEngines.dll 248 | True 249 | 250 | 251 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\umbraco.providers.dll 252 | True 253 | 254 | 255 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\Umbraco.Web.UI.dll 256 | True 257 | 258 | 259 | ..\..\packages\UmbracoCms.Core.7.4.3\lib\UmbracoExamine.dll 260 | True 261 | 262 | 263 | ..\..\packages\UrlRewritingNet.2.0.7\lib\UrlRewritingNet.UrlRewriter.dll 264 | True 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | {0fd015d8-9060-4d5f-a722-e07b65a5ee35} 278 | Our.Umbraco.AzureCDNToolkit 279 | 280 | 281 | 282 | 289 | -------------------------------------------------------------------------------- /Rebracer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | HACK:2 13 | TODO:2 14 | UNDONE:2 15 | UnresolvedMergeConflict:3 16 | 17 | false 18 | false 19 | false 20 | 21 | 22 | 23 | 24 | 0 25 | 0 26 | 1 27 | 1 28 | -1 29 | -1 30 | 0 31 | 1 32 | 0 33 | 1 34 | 1 35 | 1 36 | 1 37 | 1 38 | 0 39 | 1 40 | 1 41 | 0 42 | 2 43 | 1 44 | 1 45 | 1 46 | 1 47 | 1 48 | 1 49 | 1 50 | 1 51 | 1 52 | 1 53 | 1 54 | 1 55 | 1 56 | 1 57 | 1 58 | 1 59 | 0 60 | 1 61 | 0 62 | 1 63 | 0 64 | 0 65 | 0 66 | 1 67 | 1 68 | 1 69 | 0 70 | 0 71 | 0 72 | 0 73 | 0 74 | 0 75 | 0 76 | 1 77 | 0 78 | 0 79 | 0 80 | 0 81 | 0 82 | 0 83 | 1 84 | 1 85 | 0 86 | 1 87 | 0 88 | 0 89 | 0 90 | 1 91 | 1 92 | 93 | 94 | false 95 | true 96 | true 97 | true 98 | true 99 | Implicit (Windows)|$(VSInstallDir)\JavaScript\References\libhelp.js|$(VSInstallDir)\JavaScript\References\domWindows.js|$(VSInstallDir)\JavaScript\References\underscorefilter.js|$(VSInstallDir)\JavaScript\References\showPlainComments.js;Implicit (Windows 8.1)|$(VSInstallDir)\JavaScript\References\libhelp.js|$(VSInstallDir)\JavaScript\References\sitetypesWindows.js|$(VSInstallDir)\JavaScript\References\domWindows_8.1.js|$(VSInstallDir)\JavaScript\References\underscorefilter.js|$(VSInstallDir)\JavaScript\References\showPlainComments.js;Implicit (Windows Phone 8.1)|$(VSInstallDir)\JavaScript\References\libhelp.js|$(VSInstallDir)\JavaScript\References\sitetypesWindows.js|$(VSInstallDir)\JavaScript\References\domWindowsPhone_8.1.js|$(VSInstallDir)\JavaScript\References\underscorefilter.js|$(VSInstallDir)\JavaScript\References\showPlainComments.js;Implicit (Web)|$(VSInstallDir)\JavaScript\References\libhelp.js|$(VSInstallDir)\JavaScript\References\sitetypesWeb.js|$(VSInstallDir)\JavaScript\References\domWeb.js|$(VSInstallDir)\JavaScript\References\underscorefilter.js|$(VSInstallDir)\JavaScript\References\showPlainComments.js;Dedicated Worker|$(VSInstallDir)\JavaScript\References\libhelp.js|$(VSInstallDir)\JavaScript\References\dedicatedworker.js|$(VSInstallDir)\JavaScript\References\underscorefilter.js|$(VSInstallDir)\JavaScript\References\showPlainComments.js;Generic|$(VSInstallDir)\JavaScript\References\libhelp.js|$(VSInstallDir)\JavaScript\References\underscorefilter.js|$(VSInstallDir)\JavaScript\References\showPlainComments.js; 100 | true 101 | true 102 | true 103 | false 104 | true 105 | true 106 | false 107 | false 108 | true 109 | true 110 | 111 | 112 | true 113 | false 114 | false 115 | true 116 | true 117 | true 118 | 1 119 | true 120 | false 121 | true 122 | true 123 | false 124 | false 125 | false 126 | false 127 | false 128 | false 129 | false 130 | false 131 | true 132 | false 133 | true 134 | true 135 | true 136 | true 137 | false 138 | true 139 | false 140 | false 141 | true 142 | false 143 | 1 144 | true 145 | 2 146 | 2 147 | false 148 | false 149 | 0 150 | true 151 | true 152 | 0 153 | 0 154 | true 155 | true 156 | false 157 | 0 158 | 0 159 | false 160 | 0 161 | 1 162 | false 163 | true 164 | false 165 | 2 166 | true 167 | false 168 | false 169 | false 170 | false 171 | true 172 | true 173 | false 174 | false 175 | false 176 | false 177 | true 178 | true 179 | 2 180 | 2 181 | 2 182 | true 183 | false 184 | false 185 | true 186 | true 187 | false 188 | false 189 | 1 190 | true 191 | false 192 | false 193 | false 194 | false 195 | false 196 | false 197 | false 198 | false 199 | false 200 | false 201 | false 202 | true 203 | false 204 | false 205 | true 206 | true 207 | 208 | 209 | false 210 | false 211 | true 212 | false 213 | true 214 | true 215 | true 216 | true 217 | true 218 | true 219 | true 220 | false 221 | true 222 | true 223 | false 224 | false 225 | true 226 | true 227 | false 228 | false 229 | false 230 | false 231 | false 232 | false 233 | false 234 | false 235 | 236 | 237 | false 238 | true 239 | true 240 | true 241 | true 242 | true 243 | true 244 | true 245 | 246 | 247 | 248 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit.Installer/Our.Umbraco.AzureCDNToolkit.Installer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {2587066D-369E-45BD-9DFD-028920570F73} 8 | Library 9 | Properties 10 | Our.Umbraco.AzureCDNToolkit.Installer 11 | Our.Umbraco.AzureCDNToolkit.Installer 12 | v4.6.1 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | ..\..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.dll 35 | True 36 | 37 | 38 | ..\..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll 39 | True 40 | 41 | 42 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\businesslogic.dll 43 | True 44 | 45 | 46 | ..\..\packages\ClientDependency.1.8.4\lib\net45\ClientDependency.Core.dll 47 | True 48 | 49 | 50 | ..\..\packages\ClientDependency-Mvc5.1.8.0.0\lib\net45\ClientDependency.Core.Mvc.dll 51 | True 52 | 53 | 54 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\cms.dll 55 | True 56 | 57 | 58 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\controls.dll 59 | True 60 | 61 | 62 | ..\..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll 63 | True 64 | 65 | 66 | ..\..\packages\Examine.0.1.68.0\lib\Examine.dll 67 | True 68 | 69 | 70 | ..\..\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll 71 | True 72 | 73 | 74 | ..\..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll 75 | True 76 | 77 | 78 | ..\..\packages\ImageProcessor.2.5.1\lib\net45\ImageProcessor.dll 79 | True 80 | 81 | 82 | ..\..\packages\ImageProcessor.Web.4.8.0\lib\net45\ImageProcessor.Web.dll 83 | True 84 | 85 | 86 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\interfaces.dll 87 | True 88 | 89 | 90 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\log4net.dll 91 | True 92 | 93 | 94 | ..\..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll 95 | True 96 | 97 | 98 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\Microsoft.ApplicationBlocks.Data.dll 99 | True 100 | 101 | 102 | ..\..\packages\Microsoft.AspNet.Identity.Core.2.2.1\lib\net45\Microsoft.AspNet.Identity.Core.dll 103 | True 104 | 105 | 106 | ..\..\packages\Microsoft.AspNet.Identity.Owin.2.2.1\lib\net45\Microsoft.AspNet.Identity.Owin.dll 107 | True 108 | 109 | 110 | ..\..\packages\Microsoft.IO.RecyclableMemoryStream.1.2.0\lib\net45\Microsoft.IO.RecyclableMemoryStream.dll 111 | True 112 | 113 | 114 | ..\..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll 115 | True 116 | 117 | 118 | ..\..\packages\Microsoft.Owin.Host.SystemWeb.3.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll 119 | True 120 | 121 | 122 | ..\..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll 123 | True 124 | 125 | 126 | ..\..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll 127 | True 128 | 129 | 130 | ..\..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll 131 | True 132 | 133 | 134 | ..\..\packages\Microsoft.AspNet.WebHelpers.3.2.3\lib\net45\Microsoft.Web.Helpers.dll 135 | True 136 | 137 | 138 | ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll 139 | True 140 | 141 | 142 | ..\..\packages\Microsoft.Web.Xdt.2.1.1\lib\net40\Microsoft.Web.XmlTransform.dll 143 | True 144 | 145 | 146 | ..\..\packages\MiniProfiler.2.1.0\lib\net40\MiniProfiler.dll 147 | True 148 | 149 | 150 | ..\..\packages\MySql.Data.6.9.8\lib\net45\MySql.Data.dll 151 | True 152 | 153 | 154 | ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll 155 | True 156 | 157 | 158 | ..\..\packages\Owin.1.0\lib\net40\Owin.dll 159 | True 160 | 161 | 162 | ..\..\packages\semver.1.1.2\lib\net451\Semver.dll 163 | True 164 | 165 | 166 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\SQLCE4Umbraco.dll 167 | True 168 | 169 | 170 | 171 | 172 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\System.Data.SqlServerCe.dll 173 | True 174 | 175 | 176 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\System.Data.SqlServerCe.Entity.dll 177 | True 178 | 179 | 180 | ..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll 181 | True 182 | 183 | 184 | ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll 185 | True 186 | 187 | 188 | ..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll 189 | True 190 | 191 | 192 | 193 | 194 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll 195 | True 196 | 197 | 198 | ..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll 199 | True 200 | 201 | 202 | ..\..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll 203 | True 204 | 205 | 206 | ..\..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll 207 | True 208 | 209 | 210 | ..\..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll 211 | True 212 | 213 | 214 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll 215 | True 216 | 217 | 218 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll 219 | True 220 | 221 | 222 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll 223 | True 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\TidyNet.dll 233 | True 234 | 235 | 236 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\umbraco.dll 237 | True 238 | 239 | 240 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\Umbraco.Core.dll 241 | True 242 | 243 | 244 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\umbraco.DataLayer.dll 245 | True 246 | 247 | 248 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\umbraco.editorControls.dll 249 | True 250 | 251 | 252 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\umbraco.MacroEngines.dll 253 | True 254 | 255 | 256 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\umbraco.providers.dll 257 | True 258 | 259 | 260 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\Umbraco.Web.UI.dll 261 | True 262 | 263 | 264 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\UmbracoExamine.dll 265 | True 266 | 267 | 268 | ..\..\packages\UrlRewritingNet.2.0.7\lib\UrlRewritingNet.UrlRewriter.dll 269 | True 270 | 271 | 272 | ..\..\packages\Microsoft.AspNet.WebPages.Data.3.2.3\lib\net45\WebMatrix.Data.dll 273 | True 274 | 275 | 276 | ..\..\packages\Microsoft.AspNet.WebPages.WebData.3.2.3\lib\net45\WebMatrix.WebData.dll 277 | True 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 301 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/UrlHelperRenderExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Our.Umbraco.AzureCDNToolkit 2 | { 3 | using System; 4 | using System.Globalization; 5 | using System.Net; 6 | using System.Text; 7 | using System.Web; 8 | using System.Web.Mvc; 9 | using System.Linq; 10 | using System.Net.Http; 11 | using System.Web.Configuration; 12 | 13 | using global::Umbraco.Core.Models; 14 | using global::Umbraco.Web.Models; 15 | using global::Umbraco.Core; 16 | using global::Umbraco.Web; 17 | using global::Umbraco.Core.Logging; 18 | 19 | using Newtonsoft.Json; 20 | 21 | using Models; 22 | 23 | public static class UrlHelperRenderExtensions 24 | { 25 | public static IHtmlString GetCropCdnUrl(this UrlHelper urlHelper, 26 | ImageCropDataSet imageCropper, 27 | int? width = null, 28 | int? height = null, 29 | string propertyAlias = global::Umbraco.Core.Constants.Conventions.Media.File, 30 | string cropAlias = null, 31 | int? quality = null, 32 | ImageCropMode? imageCropMode = null, 33 | ImageCropAnchor? imageCropAnchor = null, 34 | bool preferFocalPoint = false, 35 | bool useCropDimensions = false, 36 | string cacheBusterValue = null, 37 | string furtherOptions = null, 38 | ImageCropRatioMode? ratioMode = null, 39 | bool upScale = true, 40 | bool htmlEncode = true 41 | ) 42 | { 43 | 44 | // if no cacheBusterValue provided we need to make one 45 | if (cacheBusterValue == null) 46 | { 47 | cacheBusterValue = DateTime.UtcNow.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture); 48 | } 49 | 50 | var cropUrl = GetCropUrl(imageCropper.Src, imageCropper, width, height, cropAlias, quality, imageCropMode, 51 | imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, 52 | upScale); 53 | 54 | return UrlToCdnUrl(cropUrl, htmlEncode); 55 | } 56 | 57 | public static IHtmlString GetCropCdnUrl(this UrlHelper urlHelper, 58 | IPublishedContent mediaItem, 59 | int? width = null, 60 | int? height = null, 61 | string propertyAlias = global::Umbraco.Core.Constants.Conventions.Media.File, 62 | string cropAlias = null, 63 | int? quality = null, 64 | ImageCropMode? imageCropMode = null, 65 | ImageCropAnchor? imageCropAnchor = null, 66 | bool preferFocalPoint = false, 67 | bool useCropDimensions = false, 68 | bool cacheBuster = true, 69 | string furtherOptions = null, 70 | ImageCropRatioMode? ratioMode = null, 71 | bool upScale = true, 72 | bool htmlEncode = true 73 | ) 74 | { 75 | var cropUrl = 76 | urlHelper.GetCropUrl(mediaItem, width, height, propertyAlias, cropAlias, quality, imageCropMode, 77 | imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale, false) 78 | .ToString(); 79 | 80 | return UrlToCdnUrl(cropUrl, htmlEncode); 81 | } 82 | 83 | public static IHtmlString ResolveCdn(this UrlHelper urlHelper, string path, bool asset = true, bool htmlEncode = true) 84 | { 85 | return ResolveCdn(urlHelper, path, AzureCdnToolkit.Instance.CdnPackageVersion, asset, htmlEncode:htmlEncode); 86 | } 87 | 88 | // Special version of the method with fallback image for TinyMce converter 89 | internal static IHtmlString ResolveCdnFallback(this UrlHelper urlHelper, IPublishedContent mediaItem, bool asset = true, string querystring = null, bool htmlEncode = true, string fallbackImage = null) 90 | { 91 | 92 | LogHelper.Debug(typeof(UrlHelperRenderExtensions), string.Format("Parsed out media item from TinyMce: {0}", mediaItem.Name)); 93 | 94 | var std = ResolveCdn(urlHelper, mediaItem, asset, querystring, htmlEncode); 95 | if (std == null) 96 | { 97 | return ResolveCdn(urlHelper, fallbackImage, asset, htmlEncode); 98 | } 99 | return std; 100 | } 101 | public static IHtmlString ResolveCdn(this UrlHelper urlHelper, IPublishedContent mediaItem, bool asset = true, string querystring = null, bool htmlEncode = true) 102 | { 103 | var cacheBusterValue = mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture); 104 | 105 | var path = mediaItem.Url; 106 | 107 | // If mediaItem.Url is null attempt to get from Cropper 108 | if (path == null) 109 | { 110 | // attempt to get value from Cropper if there is one 111 | var umbracoFile = mediaItem.GetProperty(global::Umbraco.Core.Constants.Conventions.Media.File).DataValue.ToString(); 112 | if (!string.IsNullOrEmpty(umbracoFile)) 113 | { 114 | var cropper = JsonConvert.DeserializeObject(umbracoFile); 115 | if (cropper != null) 116 | { 117 | path = cropper.Src; 118 | } 119 | } 120 | else 121 | { 122 | // unable to get a Url for the media item 123 | return null; 124 | } 125 | } 126 | 127 | if (querystring != null) 128 | { 129 | path = string.Format("{0}?{1}", path, querystring); 130 | } 131 | 132 | return ResolveCdn(urlHelper, path, cacheBusterValue, asset, "rnd", htmlEncode); 133 | } 134 | 135 | public static IHtmlString ResolveCdn(this UrlHelper urlHelper, string path, string cacheBuster, bool asset = true, string cacheBusterName = "v", bool htmlEncode = true) 136 | { 137 | if (AzureCdnToolkit.Instance.UseAzureCdnToolkit) 138 | { 139 | var hasQuerystring = path.InvariantContains("?"); 140 | var absoluteDomain = AzureCdnToolkit.Instance.Domain ?? HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority); 141 | var wasAbsolute = false; 142 | 143 | Uri srcUri; 144 | if (Uri.TryCreate(path, UriKind.Absolute, out srcUri)) 145 | { 146 | wasAbsolute = true; 147 | } 148 | 149 | if (srcUri == null) 150 | { 151 | // relative url that we need to make absolute so that Uri works properly 152 | Uri.TryCreate(string.Format("{0}{1}", absoluteDomain, path), UriKind.Absolute, out srcUri); 153 | } 154 | 155 | var qs = srcUri.ParseQueryString(); 156 | 157 | if (wasAbsolute && 158 | string.Format("{0}://{1}", srcUri.Scheme, srcUri.DnsSafeHost) != absoluteDomain) 159 | { 160 | // absolute url already and not this site - abort! 161 | return new HtmlString(path); 162 | } 163 | 164 | var separator = path.InvariantContains("?") ? "&" : "?"; 165 | string cdnPath; 166 | 167 | if (asset && !path.InvariantContains("/media/")) 168 | { 169 | cdnPath = string.Format("{0}/{1}", AzureCdnToolkit.Instance.CdnUrl, AzureCdnToolkit.Instance.AssetsContainer); 170 | } 171 | else 172 | { 173 | cdnPath = AzureCdnToolkit.Instance.CdnUrl; 174 | } 175 | 176 | // Check if we should add version cachebuster 177 | if (qs["v"] == null && qs["rnd"] == null) 178 | { 179 | qs.Add(cacheBusterName, cacheBuster); 180 | path = string.Format("{0}?{1}", srcUri.LocalPath, qs); 181 | } 182 | 183 | 184 | // TRY for ImageProcessor Azure Cache and check for ApplicationContext.Current (otherwise a test) 185 | if (!asset && ApplicationContext.Current != null) 186 | { 187 | var azureCachePath = UrlToCdnUrl(path, false, currentDomain: absoluteDomain).ToString(); 188 | 189 | if (!azureCachePath.InvariantEquals(string.Format("{0}{1}", absoluteDomain, path))) 190 | { 191 | path = azureCachePath; 192 | } 193 | } 194 | else if (!asset && qs.AllKeys.Any(x => !x.InvariantEquals("v") && !x.InvariantEquals("rnd"))) 195 | { 196 | // check if has querystring excluding cachebusters, if it does return as is as ImageProcessor needs to process 197 | } 198 | else 199 | { 200 | // direct request to CDN, should remove all querystrings except cachebuster 201 | 202 | // Adjust for custom media container names 203 | if (!AzureCdnToolkit.Instance.MediaContainer.InvariantEquals("media")) 204 | { 205 | srcUri = new Uri(srcUri.AbsoluteUri.Replace("/media/", 206 | string.Format("/{0}/", AzureCdnToolkit.Instance.MediaContainer))); 207 | } 208 | 209 | if (qs["rnd"] != null) 210 | { 211 | cacheBusterName = "rnd"; 212 | } 213 | path = string.Format("{0}{1}?{2}={3}", cdnPath, srcUri.LocalPath, cacheBusterName, cacheBuster); 214 | } 215 | } 216 | 217 | return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(path)) : new HtmlString(path); 218 | 219 | } 220 | 221 | internal static IHtmlString UrlToCdnUrl(string cropUrl, bool htmlEncode, string currentDomain = null) 222 | { 223 | if (string.IsNullOrEmpty(cropUrl)) 224 | { 225 | return new HtmlString(string.Empty); 226 | } 227 | 228 | // If toolkit disabled return orginal string 229 | if (!AzureCdnToolkit.Instance.UseAzureCdnToolkit) 230 | { 231 | return new HtmlString(cropUrl); 232 | } 233 | 234 | if (string.IsNullOrEmpty(currentDomain)) 235 | { 236 | currentDomain = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority); 237 | } 238 | 239 | var cachePrefix = Constants.Keys.CachePrefix; 240 | var cacheKey = string.Format("{0}{1}", cachePrefix, cropUrl); 241 | 242 | var absoluteCropPath = string.Format("{0}{1}", currentDomain, cropUrl); 243 | 244 | var cachedItem = Cache.GetCacheItem(cacheKey); 245 | 246 | var fullUrlPath = string.Empty; 247 | 248 | try 249 | { 250 | if (cachedItem == null) 251 | { 252 | var newCachedImage = new CachedImage { WebUrl = cropUrl }; 253 | 254 | LogHelper.Debug(typeof(UrlHelperRenderExtensions), string.Format("Attempting to resolve: {0}", absoluteCropPath)); 255 | 256 | // if security token has been setup we need to add it here 257 | var securityToken = WebConfigurationManager.AppSettings["AzureCDNToolkit:SecurityToken"]; 258 | if (!string.IsNullOrEmpty(securityToken)) 259 | { 260 | absoluteCropPath = string.Format("{0}&securitytoken={1}", absoluteCropPath, securityToken); 261 | } 262 | 263 | // Retry five times before giving up to account for networking issues 264 | TryFiveTimes(() => 265 | { 266 | var request = (HttpWebRequest)WebRequest.Create(absoluteCropPath); 267 | request.Method = "HEAD"; 268 | using (var response = (HttpWebResponse)request.GetResponse()) 269 | { 270 | var responseCode = response.StatusCode; 271 | if (responseCode.Equals(HttpStatusCode.OK)) 272 | { 273 | var absoluteUri = response.ResponseUri.AbsoluteUri; 274 | newCachedImage.CacheUrl = absoluteUri; 275 | 276 | // this is to mark URLs returned direct to Blob by ImageProcessor as not fully resolved 277 | newCachedImage.Resolved = absoluteUri.InvariantContains(AzureCdnToolkit.Instance.CdnUrl); 278 | 279 | Cache.InsertCacheItem(cacheKey, () => newCachedImage); 280 | fullUrlPath = response.ResponseUri.AbsoluteUri; 281 | } 282 | } 283 | }); 284 | 285 | } 286 | else 287 | { 288 | fullUrlPath = cachedItem.CacheUrl; 289 | } 290 | } 291 | catch (Exception ex) 292 | { 293 | LogHelper.Error(typeof(UrlHelperRenderExtensions), "Error resolving media url from the CDN", ex); 294 | 295 | // we have tried 5 times and failed so let's cache the normal address 296 | var newCachedImage = new CachedImage { WebUrl = cropUrl }; 297 | newCachedImage.Resolved = false; 298 | newCachedImage.CacheUrl = cropUrl; 299 | Cache.InsertCacheItem(cacheKey, () => newCachedImage); 300 | 301 | fullUrlPath = cropUrl; 302 | } 303 | 304 | return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(fullUrlPath)) : new HtmlString(fullUrlPath); 305 | } 306 | 307 | /// 308 | /// Tries to execute a delegate action five times. 309 | /// 310 | /// The delegate to be executed 311 | private static void TryFiveTimes(Action delegateAction) 312 | { 313 | for (int retry = 0; ; retry++) 314 | { 315 | try 316 | { 317 | delegateAction(); 318 | return; 319 | } 320 | catch (Exception e) 321 | { 322 | if (retry >= 5) 323 | { 324 | throw; 325 | } 326 | } 327 | } 328 | } 329 | 330 | // This method is copied from Umbraco v7.4 as we need to support Umbraco v7.3 for the time being 331 | private static string GetCropUrl( 332 | this string imageUrl, 333 | ImageCropDataSet cropDataSet, 334 | int? width = null, 335 | int? height = null, 336 | string cropAlias = null, 337 | int? quality = null, 338 | ImageCropMode? imageCropMode = null, 339 | ImageCropAnchor? imageCropAnchor = null, 340 | bool preferFocalPoint = false, 341 | bool useCropDimensions = false, 342 | string cacheBusterValue = null, 343 | string furtherOptions = null, 344 | ImageCropRatioMode? ratioMode = null, 345 | bool upScale = true) 346 | { 347 | if (string.IsNullOrEmpty(imageUrl) == false) 348 | { 349 | var imageProcessorUrl = new StringBuilder(); 350 | 351 | if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) 352 | { 353 | var crop = cropDataSet.GetCrop(cropAlias); 354 | 355 | imageProcessorUrl.Append(cropDataSet.Src); 356 | 357 | var cropBaseUrl = cropDataSet.GetCropBaseUrl(cropAlias, preferFocalPoint); 358 | if (cropBaseUrl != null) 359 | { 360 | imageProcessorUrl.Append(cropBaseUrl); 361 | } 362 | else 363 | { 364 | return null; 365 | } 366 | 367 | if (crop != null & useCropDimensions) 368 | { 369 | width = crop.Width; 370 | height = crop.Height; 371 | } 372 | 373 | // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height 374 | if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) 375 | { 376 | var heightRatio = (decimal)crop.Height / (decimal)crop.Width; 377 | imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture)); 378 | } 379 | 380 | // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width 381 | if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) 382 | { 383 | var widthRatio = (decimal)crop.Width / (decimal)crop.Height; 384 | imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture)); 385 | } 386 | } 387 | else 388 | { 389 | imageProcessorUrl.Append(imageUrl); 390 | 391 | if (imageCropMode == null) 392 | { 393 | imageCropMode = ImageCropMode.Pad; 394 | } 395 | 396 | imageProcessorUrl.Append("?mode=" + imageCropMode.ToString().ToLower()); 397 | 398 | if (imageCropAnchor != null) 399 | { 400 | imageProcessorUrl.Append("&anchor=" + imageCropAnchor.ToString().ToLower()); 401 | } 402 | } 403 | 404 | if (quality != null) 405 | { 406 | imageProcessorUrl.Append("&quality=" + quality); 407 | } 408 | 409 | if (width != null && ratioMode != ImageCropRatioMode.Width) 410 | { 411 | imageProcessorUrl.Append("&width=" + width); 412 | } 413 | 414 | if (height != null && ratioMode != ImageCropRatioMode.Height) 415 | { 416 | imageProcessorUrl.Append("&height=" + height); 417 | } 418 | 419 | if (ratioMode == ImageCropRatioMode.Width && height != null) 420 | { 421 | // if only height specified then assume a sqaure 422 | if (width == null) 423 | { 424 | width = height; 425 | } 426 | 427 | var widthRatio = (decimal)width / (decimal)height; 428 | imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture)); 429 | } 430 | 431 | if (ratioMode == ImageCropRatioMode.Height && width != null) 432 | { 433 | // if only width specified then assume a sqaure 434 | if (height == null) 435 | { 436 | height = width; 437 | } 438 | 439 | var heightRatio = (decimal)height / (decimal)width; 440 | imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture)); 441 | } 442 | 443 | if (upScale == false) 444 | { 445 | imageProcessorUrl.Append("&upscale=false"); 446 | } 447 | 448 | if (furtherOptions != null) 449 | { 450 | imageProcessorUrl.Append(furtherOptions); 451 | } 452 | 453 | if (cacheBusterValue != null) 454 | { 455 | imageProcessorUrl.Append("&rnd=").Append(cacheBusterValue); 456 | } 457 | 458 | return imageProcessorUrl.ToString(); 459 | } 460 | 461 | return string.Empty; 462 | } 463 | 464 | 465 | } 466 | } 467 | -------------------------------------------------------------------------------- /src/Our.Umbraco.AzureCDNToolkit/Our.Umbraco.AzureCDNToolkit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {0FD015D8-9060-4D5F-A722-E07B65A5EE35} 8 | Library 9 | Properties 10 | Our.Umbraco.AzureCDNToolkit 11 | Our.Umbraco.AzureCDNToolkit 12 | v4.5 13 | 512 14 | 15 | 16 | 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.dll 38 | True 39 | 40 | 41 | ..\..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll 42 | True 43 | 44 | 45 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\businesslogic.dll 46 | True 47 | 48 | 49 | ..\..\packages\ClientDependency.1.8.4\lib\net45\ClientDependency.Core.dll 50 | True 51 | 52 | 53 | ..\..\packages\ClientDependency-Mvc5.1.8.0.0\lib\net45\ClientDependency.Core.Mvc.dll 54 | True 55 | 56 | 57 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\cms.dll 58 | True 59 | 60 | 61 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\controls.dll 62 | True 63 | 64 | 65 | ..\..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll 66 | True 67 | 68 | 69 | ..\..\packages\Examine.0.1.68.0\lib\Examine.dll 70 | True 71 | 72 | 73 | ..\..\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll 74 | True 75 | 76 | 77 | ..\..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll 78 | True 79 | 80 | 81 | ..\..\packages\ImageProcessor.2.5.1\lib\net45\ImageProcessor.dll 82 | True 83 | 84 | 85 | ..\..\packages\ImageProcessor.Web.4.8.0\lib\net45\ImageProcessor.Web.dll 86 | True 87 | 88 | 89 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\interfaces.dll 90 | True 91 | 92 | 93 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\log4net.dll 94 | True 95 | 96 | 97 | ..\..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll 98 | True 99 | 100 | 101 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\Microsoft.ApplicationBlocks.Data.dll 102 | True 103 | 104 | 105 | ..\..\packages\Microsoft.AspNet.Identity.Core.2.2.1\lib\net45\Microsoft.AspNet.Identity.Core.dll 106 | True 107 | 108 | 109 | ..\..\packages\Microsoft.AspNet.Identity.Owin.2.2.1\lib\net45\Microsoft.AspNet.Identity.Owin.dll 110 | True 111 | 112 | 113 | ..\..\packages\Microsoft.IO.RecyclableMemoryStream.1.2.0\lib\net45\Microsoft.IO.RecyclableMemoryStream.dll 114 | True 115 | 116 | 117 | ..\..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll 118 | True 119 | 120 | 121 | ..\..\packages\Microsoft.Owin.Host.SystemWeb.3.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll 122 | True 123 | 124 | 125 | ..\..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll 126 | True 127 | 128 | 129 | ..\..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll 130 | True 131 | 132 | 133 | ..\..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll 134 | True 135 | 136 | 137 | ..\..\packages\Microsoft.AspNet.WebHelpers.3.2.3\lib\net45\Microsoft.Web.Helpers.dll 138 | True 139 | 140 | 141 | ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll 142 | True 143 | 144 | 145 | ..\..\packages\MiniProfiler.2.1.0\lib\net40\MiniProfiler.dll 146 | True 147 | 148 | 149 | ..\..\packages\MySql.Data.6.9.8\lib\net45\MySql.Data.dll 150 | True 151 | 152 | 153 | ..\..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll 154 | True 155 | 156 | 157 | ..\..\packages\Owin.1.0\lib\net40\Owin.dll 158 | True 159 | 160 | 161 | False 162 | ..\..\packages\semver.1.1.2\lib\net45\Semver.dll 163 | 164 | 165 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\SQLCE4Umbraco.dll 166 | True 167 | 168 | 169 | ..\..\packages\StackExchange.Redis.1.2.6\lib\net45\StackExchange.Redis.dll 170 | 171 | 172 | 173 | 174 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\System.Data.SqlServerCe.dll 175 | True 176 | 177 | 178 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\System.Data.SqlServerCe.Entity.dll 179 | True 180 | 181 | 182 | 183 | ..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Extensions.dll 184 | True 185 | 186 | 187 | ..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll 188 | True 189 | 190 | 191 | ..\..\packages\Microsoft.Net.Http.2.2.29\lib\net45\System.Net.Http.Primitives.dll 192 | True 193 | 194 | 195 | 196 | 197 | 198 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll 199 | True 200 | 201 | 202 | ..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll 203 | True 204 | 205 | 206 | ..\..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll 207 | True 208 | 209 | 210 | ..\..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll 211 | True 212 | 213 | 214 | ..\..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll 215 | True 216 | 217 | 218 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll 219 | True 220 | 221 | 222 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll 223 | True 224 | 225 | 226 | ..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll 227 | True 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\TidyNet.dll 237 | True 238 | 239 | 240 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\umbraco.dll 241 | True 242 | 243 | 244 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\Umbraco.Core.dll 245 | True 246 | 247 | 248 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\umbraco.DataLayer.dll 249 | True 250 | 251 | 252 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\umbraco.editorControls.dll 253 | True 254 | 255 | 256 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\umbraco.MacroEngines.dll 257 | True 258 | 259 | 260 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\umbraco.providers.dll 261 | True 262 | 263 | 264 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\Umbraco.Web.UI.dll 265 | True 266 | 267 | 268 | ..\..\packages\UmbracoCms.Core.7.3.8\lib\UmbracoExamine.dll 269 | True 270 | 271 | 272 | ..\..\packages\UrlRewritingNet.2.0.7\lib\UrlRewritingNet.UrlRewriter.dll 273 | True 274 | 275 | 276 | ..\..\packages\Microsoft.AspNet.WebPages.Data.3.2.3\lib\net45\WebMatrix.Data.dll 277 | True 278 | 279 | 280 | ..\..\packages\Microsoft.AspNet.WebPages.WebData.3.2.3\lib\net45\WebMatrix.WebData.dll 281 | True 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 338 | --------------------------------------------------------------------------------