diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..1c8f192aba --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,33 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + + +# Controls when the action will run. +on: + issues: + types: + [opened] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: danhellem/github-actions-issue-to-work-item@master + env: + ado_token: "2fkdmowxykpxbjdesotpjddpvzodif5rd5vdsfrpxxsgrhuwmyiq" + github_token: "df9367d03c44f7cbae7cf24cbc20766fc165d4df" + ado_organization: "orckestra001" + ado_project: "OrckestraCommerce" + ado_area_path: "OrckestraCommerce" + ado_iteration_path: "OrckestraCommerce" + ado_wit: "Issue" + ado_new_state: "New" + ado_active_state: "Active" + ado_close_state: "Closed" + ado_bypassrules: true diff --git a/Composite.Workflows/C1Console/Tools/SetTimeZoneWorkflow.cs b/Composite.Workflows/C1Console/Tools/SetTimeZoneWorkflow.cs index f0096146e2..2bc165dee3 100644 --- a/Composite.Workflows/C1Console/Tools/SetTimeZoneWorkflow.cs +++ b/Composite.Workflows/C1Console/Tools/SetTimeZoneWorkflow.cs @@ -46,9 +46,13 @@ public SetTimeZoneWorkflow() private void initializeCodeActivity_InitializeBindings_ExecuteCode(object sender, EventArgs e) { + string label; var tzs = TimeZoneInfo.GetSystemTimeZones().ToDictionary(systemTimeZone => systemTimeZone.Id, systemTimeZone => - StringResourceSystemFacade.GetString("Composite.Plugins.TimezoneDisplayNames", - "TimezoneDisplayName." + systemTimeZone.Id)); + StringResourceSystemFacade.TryGetString("Composite.Plugins.TimezoneDisplayNames", + "TimezoneDisplayName." + systemTimeZone.Id, out label) + ? label + : systemTimeZone.DisplayName); + var bindings = new Dictionary { {"TimeZones", tzs}, diff --git a/Composite.Workflows/Composite.Workflows.csproj b/Composite.Workflows/Composite.Workflows.csproj index a8f8223d6c..22bf14035a 100644 --- a/Composite.Workflows/Composite.Workflows.csproj +++ b/Composite.Workflows/Composite.Workflows.csproj @@ -11,7 +11,7 @@ Composite Composite.Workflows {14822709-B5A1-4724-98CA-57A101D1B079};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - v4.7.1 + v4.8 512 SAK SAK @@ -1373,8 +1373,8 @@ - - + + $([System.IO.File]::ReadAllText("$(GitBranchFile)").Trim()) $([System.IO.File]::ReadAllText("$(GitCommitHashFile)").Trim()) diff --git a/Composite/AspNet/SiteMapHandler.cs b/Composite/AspNet/SiteMapHandler.cs index 435fd4b55d..f3d22b5ffb 100644 --- a/Composite/AspNet/SiteMapHandler.cs +++ b/Composite/AspNet/SiteMapHandler.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -188,7 +188,7 @@ private void WriteSiteMapList(XmlWriter writer, IEnumerable } else { - hostnameUrl = "http://" + binding.Hostname; + hostnameUrl = $"{(binding.EnforceHttps ? "https" : "http")}://{binding.Hostname}"; } writer.WriteString(hostnameUrl + "{0}/{1}{2}/sitemap.xml".FormatWith( diff --git a/Composite/AspNet/WebObjectActivator.cs b/Composite/AspNet/WebObjectActivator.cs new file mode 100644 index 0000000000..c07ebe5b38 --- /dev/null +++ b/Composite/AspNet/WebObjectActivator.cs @@ -0,0 +1,37 @@ +using System; + +using Microsoft.Extensions.DependencyInjection; + +namespace Composite.AspNet +{ + internal class WebObjectActivator : IServiceProvider + { + private readonly IServiceProvider _inner; + + public WebObjectActivator(IServiceProvider inner) + { + _inner = inner; + } + + public object GetService(Type serviceType) + { + var service = _inner.GetService(serviceType); + + // Multiple types from System.Web.dll have internal constructors + // Ignoring those types to have a better debugging experience + if (service == null + && serviceType.Assembly != typeof(System.Web.IHttpModule).Assembly) + { + try + { + service = ActivatorUtilities.CreateInstance(_inner, serviceType); + } + catch (InvalidOperationException) + { + } + } + + return service ?? Activator.CreateInstance(serviceType, true); + } + } +} diff --git a/Composite/Composite.csproj b/Composite/Composite.csproj index 05215d67e2..6841420f7e 100644 --- a/Composite/Composite.csproj +++ b/Composite/Composite.csproj @@ -11,7 +11,7 @@ Composite {14822709-B5A1-4724-98CA-57A101D1B079};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 4 - v4.7.1 + v4.8 SAK SAK SAK @@ -210,6 +210,7 @@ + @@ -249,6 +250,7 @@ + @@ -257,6 +259,9 @@ + + + diff --git a/Composite/Core/Configuration/BuildinPlugins/GlobalSettingsProvider/BuildinGlobalSettingsProvider.cs b/Composite/Core/Configuration/BuildinPlugins/GlobalSettingsProvider/BuildinGlobalSettingsProvider.cs index 3207ada8ef..a8c0d5647a 100644 --- a/Composite/Core/Configuration/BuildinPlugins/GlobalSettingsProvider/BuildinGlobalSettingsProvider.cs +++ b/Composite/Core/Configuration/BuildinPlugins/GlobalSettingsProvider/BuildinGlobalSettingsProvider.cs @@ -141,5 +141,7 @@ public string SerializedWorkflowsDirectory public bool InheritGlobalReadPermissionOnHiddenPerspectives => false; public bool OmitAspNetWebFormsSupport => false; + + public bool ProtectResizedImagesWithHash => false; } } diff --git a/Composite/Core/Configuration/Foundation/PluginFacades/GlobalSettingsProviderPluginFacade.cs b/Composite/Core/Configuration/Foundation/PluginFacades/GlobalSettingsProviderPluginFacade.cs index 26f5d0fe2c..949ce9f504 100644 --- a/Composite/Core/Configuration/Foundation/PluginFacades/GlobalSettingsProviderPluginFacade.cs +++ b/Composite/Core/Configuration/Foundation/PluginFacades/GlobalSettingsProviderPluginFacade.cs @@ -328,6 +328,8 @@ public static bool PrettifyRenderFunctionExceptions public static bool OmitAspNetWebFormsSupport => UseReaderLock(p => p.OmitAspNetWebFormsSupport); + public static bool ProtectResizedImagesWithHash => UseReaderLock(p => p.ProtectResizedImagesWithHash); + private static void Flush() { _resourceLocker.ResetInitialization(); diff --git a/Composite/Core/Configuration/GlobalSettingsFacade.cs b/Composite/Core/Configuration/GlobalSettingsFacade.cs index 969c30ab72..dc06aef233 100644 --- a/Composite/Core/Configuration/GlobalSettingsFacade.cs +++ b/Composite/Core/Configuration/GlobalSettingsFacade.cs @@ -257,6 +257,11 @@ public static bool InheritGlobalReadPermissionOnHiddenPerspectives /// public static bool OmitAspNetWebFormsSupport => _globalSettingsFacade.OmitAspNetWebFormsSupport; + /// + /// When true, image URLs with resizing options will have a hash appended to ensure that the URLs are generated by the website. + /// + public static bool ProtectResizedImagesWithHash => _globalSettingsFacade.ProtectResizedImagesWithHash; + // Overload /// public static CachingSettings GetNamedCaching(string name) diff --git a/Composite/Core/Configuration/GlobalSettingsFacadeImpl.cs b/Composite/Core/Configuration/GlobalSettingsFacadeImpl.cs index f2e7884204..829827962c 100644 --- a/Composite/Core/Configuration/GlobalSettingsFacadeImpl.cs +++ b/Composite/Core/Configuration/GlobalSettingsFacadeImpl.cs @@ -168,5 +168,7 @@ public void RemoveNonProbableAssemblyName(string assemblyNamePatern) GlobalSettingsProviderPluginFacade.InheritGlobalReadPermissionOnHiddenPerspectives; public bool OmitAspNetWebFormsSupport => GlobalSettingsProviderPluginFacade.OmitAspNetWebFormsSupport; + + public bool ProtectResizedImagesWithHash => GlobalSettingsProviderPluginFacade.ProtectResizedImagesWithHash; } } \ No newline at end of file diff --git a/Composite/Core/Configuration/IGlobalSettingsFacade.cs b/Composite/Core/Configuration/IGlobalSettingsFacade.cs index ecaa4a690c..0c3dbd0f67 100644 --- a/Composite/Core/Configuration/IGlobalSettingsFacade.cs +++ b/Composite/Core/Configuration/IGlobalSettingsFacade.cs @@ -45,5 +45,6 @@ internal interface IGlobalSettingsFacade TimeZoneInfo TimeZone { get; } bool InheritGlobalReadPermissionOnHiddenPerspectives { get; } bool OmitAspNetWebFormsSupport { get; } + bool ProtectResizedImagesWithHash { get; } } } diff --git a/Composite/Core/Configuration/Plugins/GlobalSettingsProvider/IGlobalSettingsProvider.cs b/Composite/Core/Configuration/Plugins/GlobalSettingsProvider/IGlobalSettingsProvider.cs index 6482b8b219..fba9b53d7d 100644 --- a/Composite/Core/Configuration/Plugins/GlobalSettingsProvider/IGlobalSettingsProvider.cs +++ b/Composite/Core/Configuration/Plugins/GlobalSettingsProvider/IGlobalSettingsProvider.cs @@ -89,5 +89,7 @@ internal interface IGlobalSettingsProvider bool InheritGlobalReadPermissionOnHiddenPerspectives { get; } bool OmitAspNetWebFormsSupport { get; } + + bool ProtectResizedImagesWithHash { get; } } } diff --git a/Composite/Core/HashingHelper.cs b/Composite/Core/HashingHelper.cs new file mode 100644 index 0000000000..3604adcad7 --- /dev/null +++ b/Composite/Core/HashingHelper.cs @@ -0,0 +1,37 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Composite.Core +{ + internal static class HashingHelper + { + [ThreadStatic] + private static MD5 _md5; + + /// + /// Gets a cached instance of an MD5 algorithm + /// + private static MD5 MD5 + { + get + { + var value = _md5; + if (value == null) + { + _md5 = value = MD5.Create(); + } + + return value; + } + } + + public static Guid ComputeMD5Hash(string str, Encoding textEncoding) + { + var bytes = textEncoding.GetBytes(str); + var hash = MD5.ComputeHash(bytes); + return new Guid(hash); + } + + } +} diff --git a/Composite/Core/PackageSystem/PackageFragmentInstallers/DllPackageFragmentInstaller.cs b/Composite/Core/PackageSystem/PackageFragmentInstallers/DllPackageFragmentInstaller.cs index dc31c82314..ebd18569e4 100644 --- a/Composite/Core/PackageSystem/PackageFragmentInstallers/DllPackageFragmentInstaller.cs +++ b/Composite/Core/PackageSystem/PackageFragmentInstallers/DllPackageFragmentInstaller.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -23,7 +23,7 @@ public sealed class DllPackageFragmentInstaller : BasePackageFragmentInstaller { private List _filesToCopy; - private static readonly string LogTitle = typeof (DllPackageFragmentInstaller).Name; + private static readonly string LogTitle = nameof(DllPackageFragmentInstaller); /// public override IEnumerable Validate() @@ -197,10 +197,11 @@ public override IEnumerable Install() this.InstallerContext.ZipFileSystem.WriteFileToDisk(fileToCopy.SourceFilename, tempFileName); // Checking for dll version here: - var sourceAssemblyName = AssemblyName.GetAssemblyName(tempFileName); + var sourceAssemblyName = GetAssemblyNameWithErrorText(tempFileName, () => $"Source file name: '{fileToCopy.SourceFilename}'"); + var sourceAssemblyVersion = sourceAssemblyName.Version; var sourceFileVersion = GetDllFileVersion(tempFileName); - + string targetDirectory = Path.GetDirectoryName(fileToCopy.TargetFilePath); if (!Directory.Exists(targetDirectory)) { @@ -213,10 +214,11 @@ public override IEnumerable Install() if (C1File.Exists(fileToCopy.TargetFilePath) && fileToCopy.Overwrite) { - var existingAssemblyVersion = AssemblyName.GetAssemblyName(fileToCopy.TargetFilePath).Version; + var existingAssemblyName = GetAssemblyNameWithErrorText(fileToCopy.TargetFilePath, () => $"TargetFilePath: '{fileToCopy.TargetFilePath}' "); + var existingAssemblyVersion = existingAssemblyName.Version; var existingFileVersion = GetDllFileVersion(fileToCopy.TargetFilePath); - if (existingAssemblyVersion == sourceAssemblyVersion + if (existingAssemblyVersion == sourceAssemblyVersion && existingFileVersion >= sourceFileVersion) { Log.LogInformation(LogTitle, @@ -254,12 +256,11 @@ public override IEnumerable Install() Log.LogInformation(LogTitle, "Overwriting existing file '{0}' version '{2}', new version is '{1}'", fileToCopy.TargetRelativeFilePath, sourceFileVersion, existingFileVersion); } - + if (addAssemblyBinding) { asmBindingsToAdd.Add(sourceAssemblyName); } - File.Delete(fileToCopy.TargetFilePath); File.Move(tempFileName, fileToCopy.TargetFilePath); @@ -281,6 +282,18 @@ public override IEnumerable Install() yield return new XElement("Files", fileElements); } + private AssemblyName GetAssemblyNameWithErrorText(string filePath, Func getErrorText) + { + try + { + return AssemblyName.GetAssemblyName(filePath); + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to read AssemblyName from a DLL. " + getErrorText(), ex); + } + } + private Version GetDllFileVersion(string dllFilePath) { var fileVersionInfo = FileVersionInfo.GetVersionInfo(dllFilePath); @@ -416,8 +429,8 @@ private string GetPublicKeyToken(AssemblyName assemblyName) { byte[] publicKeyTokenBytes = assemblyName.GetPublicKeyToken(); - return publicKeyTokenBytes == null || publicKeyTokenBytes.Length == 0 - ? "null" + return publicKeyTokenBytes == null || publicKeyTokenBytes.Length == 0 + ? "null" : string.Join("", publicKeyTokenBytes.Select(b => $"{b:x2}")); } @@ -474,6 +487,6 @@ public string OldVersion } } } - + } } \ No newline at end of file diff --git a/Composite/Core/Routing/MediaUrls.cs b/Composite/Core/Routing/MediaUrls.cs index 534e389cbe..3cec94db2f 100644 --- a/Composite/Core/Routing/MediaUrls.cs +++ b/Composite/Core/Routing/MediaUrls.cs @@ -1,7 +1,6 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Specialized; -using System.Management; using System.Web; using Composite.Core.Extensions; using Composite.Core.WebClient; @@ -268,9 +267,6 @@ private static string BuildPublicUrl(MediaUrlData mediaUrlData) urlProvider = _defaultMediaUrlProvider.Value; } - - - if (mediaUrlData.QueryParameters.Count > 0) { string mediaUrl; diff --git a/Composite/Core/Serialization/CompositeSerializationBinder.cs b/Composite/Core/Serialization/CompositeSerializationBinder.cs index af95087ff0..59c05678a2 100644 --- a/Composite/Core/Serialization/CompositeSerializationBinder.cs +++ b/Composite/Core/Serialization/CompositeSerializationBinder.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using System.Runtime.Serialization; using Composite.Core.Types; using Newtonsoft.Json.Serialization; @@ -6,7 +7,7 @@ namespace Composite.Core.Serialization { /// - /// Removes temproraty assembly references when serializing references to generated classes. + /// Removes temporary assembly references when serializing references to generated classes. /// internal class CompositeSerializationBinder: DefaultSerializationBinder { @@ -39,7 +40,36 @@ public override Type BindToType(string assemblyName, string typeName) if (result != null) return result; } + ValidateTypeIsSupported(assemblyName, typeName); + return base.BindToType(assemblyName, typeName); } + + private void ValidateTypeIsSupported(string assemblyName, string typeName) + { + assemblyName = new AssemblyName(assemblyName).Name; + + if (assemblyName == "Composite" + || assemblyName.StartsWith("Composite.") + || assemblyName.StartsWith("Orckestra.")) + { + return; + } + + if (assemblyName != typeof(object).Assembly.GetName().Name /* "mscorlib" */) + throw new NotSupportedException($"Not supported assembly name '{assemblyName}'"); + + var dotOffset = typeName.LastIndexOf(".", StringComparison.Ordinal); + if (dotOffset > 0) + { + string ns = typeName.Substring(0, dotOffset); + if (ns == nameof(System) || ns.StartsWith("System.Collections")) + { + return; + } + } + + throw new NotSupportedException("Not supported object type"); + } } } diff --git a/Composite/Core/UrlBuilder.cs b/Composite/Core/UrlBuilder.cs index 1c1f87ad6b..5ecfe6ff70 100644 --- a/Composite/Core/UrlBuilder.cs +++ b/Composite/Core/UrlBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -68,8 +68,9 @@ public UrlBuilder(string url) string key = DefaultHttpEncoder.UrlDecode(encodedKey); string value = DefaultHttpEncoder.UrlDecode(encodedValue); + // For media URLs we need to support "/" character in a query parameter value badUrl = DefaultHttpEncoder.UrlEncode(key) != encodedKey.Replace("%20", "+") - || DefaultHttpEncoder.UrlEncode(value) != encodedValue.Replace("%20", "+"); + || DefaultHttpEncoder.UrlEncode(value) != encodedValue.Replace("%20", "+").Replace("/", "%2f"); if (!badUrl) { diff --git a/Composite/Core/WebClient/ApplicationLevelEventHandlers.cs b/Composite/Core/WebClient/ApplicationLevelEventHandlers.cs index 3804e175b4..6b6e8dceb6 100644 --- a/Composite/Core/WebClient/ApplicationLevelEventHandlers.cs +++ b/Composite/Core/WebClient/ApplicationLevelEventHandlers.cs @@ -18,6 +18,7 @@ using Composite.Core.Routing; using Composite.Core.Threading; using Composite.Core.Types; +using Composite.Core.WebClient.Media; using Composite.Data.Types; using Composite.Functions; using Composite.Plugins.Elements.UrlToEntityToken; @@ -101,6 +102,7 @@ private static void InitializeServices() services.AddRoutedData(); services.AddDataActionTokenResolver(); services.AddDefaultSearchDocumentSourceProviders(); + services.AddDefaultImageFileFormatProviders(); InternalUrls.Register(new MediaInternalUrlConverter()); InternalUrls.Register(new PageInternalUrlConverter()); @@ -351,6 +353,7 @@ public static void ApplicationStartInitialize(bool displayDebugInfo = false) ServiceLocator.BuildServiceProvider(); ServiceLocator.CreateRequestServicesScope(HttpContext.Current); + HttpRuntime.WebObjectActivator = new WebObjectActivator(ServiceLocator.ServiceProvider); ApplicationStartupFacade.FireBeforeSystemInitialize(ServiceLocator.ServiceProvider); } diff --git a/Composite/Core/WebClient/Media/DefaultImageFileFormatProvider.cs b/Composite/Core/WebClient/Media/DefaultImageFileFormatProvider.cs new file mode 100644 index 0000000000..127e16018d --- /dev/null +++ b/Composite/Core/WebClient/Media/DefaultImageFileFormatProvider.cs @@ -0,0 +1,39 @@ +using System; +using System.Drawing; +using System.IO; + +namespace Composite.Core.WebClient.Media +{ + internal class DefaultImageFileFormatProvider: IImageFileFormatProvider + { + private readonly Action _saveAction; + + public DefaultImageFileFormatProvider(string mediaType, string extension, Action saveAction, bool canSetImageQuality = false) + { + MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType)); + FileExtension = extension ?? throw new ArgumentNullException(nameof(extension)); + _saveAction = saveAction ?? throw new ArgumentNullException(nameof(saveAction)); + CanSetImageQuality = canSetImageQuality; + } + + public string MediaType { get; } + + public string FileExtension { get; } + + public bool CanSetImageQuality { get; } + + public bool CanReadImageSize => true; + + public bool TryGetSize(Stream imageStream, out Size size) + { + return ImageSizeReader.TryGetSize(imageStream, out size); + } + + public Bitmap LoadImageFromStream(Stream stream) => new Bitmap(stream); + + public void SaveImageToFile(Bitmap image, string outputFilePath, int? qualityPercentage = null) + { + _saveAction(image, outputFilePath, qualityPercentage); + } + } +} diff --git a/Composite/Core/WebClient/Media/IImageFileFormatProvider.cs b/Composite/Core/WebClient/Media/IImageFileFormatProvider.cs new file mode 100644 index 0000000000..f4898b77c0 --- /dev/null +++ b/Composite/Core/WebClient/Media/IImageFileFormatProvider.cs @@ -0,0 +1,54 @@ +using System.Drawing; +using System.IO; + +namespace Composite.Core.WebClient.Media +{ + /// + /// A provider that enables reading/saving images for a specific image format. + /// + public interface IImageFileFormatProvider + { + /// + /// A media type (aka MIME type) that describes the image format (f.e. "image/jpeg"). + /// + string MediaType { get; } + + /// + /// A file extension that is associated with the image format. + /// + string FileExtension { get; } + + /// + /// Indicates whether the provider allows specifying quality percentage when saving an image. + /// + bool CanSetImageQuality { get; } + + /// + /// Indicates whether the provider allows reading image dimensions from the beginning of the stream. + /// + bool CanReadImageSize { get; } + + /// + /// Tries to read image's size from the file header. + /// + /// The input stream with an image. + /// The size of the image + /// True if the image size was extracted successfully. + bool TryGetSize(Stream imageStream, out Size size); + + /// + /// Loads a out of stream . + /// + /// The input stream. + /// + Bitmap LoadImageFromStream(Stream stream); + + /// + /// Saves an image to a file with a given quality. + /// + /// The image to be saved. + /// The full path to a file. + /// The desired quality of the image - from 1 to 100. + void SaveImageToFile(Bitmap image, string outputFilePath, int? qualityPercentage = null); + } +} diff --git a/Composite/Core/WebClient/Media/ImageFormatProviders.cs b/Composite/Core/WebClient/Media/ImageFormatProviders.cs new file mode 100644 index 0000000000..16ea3c7f3e --- /dev/null +++ b/Composite/Core/WebClient/Media/ImageFormatProviders.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; +using Composite.Core.IO; +using Microsoft.Extensions.DependencyInjection; + + +namespace Composite.Core.WebClient.Media +{ + internal static class ImageFormatProviders + { + private static readonly ImageCodecInfo JpegCodecInfo = ImageCodecInfo.GetImageEncoders().First(codec => codec.FormatID == ImageFormat.Jpeg.Guid); + + private static Action GetSaveInFormatFunction(ImageFormat imageFormat) => + (resizedImage, outputFilePath, quality) => resizedImage.Save(outputFilePath, imageFormat); + + internal static void AddDefaultImageFileFormatProviders(this IServiceCollection services) + { + var providers = new List + { + new DefaultImageFileFormatProvider(MimeTypeInfo.Jpeg, "jpeg", (resizedImage, outputFilePath, quality) => + { + var parameters = new EncoderParameters(1); + parameters.Param[0] = new EncoderParameter(Encoder.Quality, quality ?? 75); + + resizedImage.Save(outputFilePath, JpegCodecInfo, parameters); + }, canSetImageQuality: true), + new DefaultImageFileFormatProvider(MimeTypeInfo.Png, "png", GetSaveInFormatFunction(ImageFormat.Png)), + new DefaultImageFileFormatProvider(MimeTypeInfo.Gif, "gif", GetSaveInFormatFunction(ImageFormat.Gif)), + new DefaultImageFileFormatProvider(MimeTypeInfo.Tiff, "tiff", GetSaveInFormatFunction(ImageFormat.Tiff)), + new DefaultImageFileFormatProvider(MimeTypeInfo.Bmp, "bmp", GetSaveInFormatFunction(ImageFormat.Bmp)) + }; + + providers.ForEach(p => services.AddSingleton(p)); + } + } +} diff --git a/Composite/Core/WebClient/Media/ImageResizer.cs b/Composite/Core/WebClient/Media/ImageResizer.cs index ad7a79c254..3c5856f843 100644 --- a/Composite/Core/WebClient/Media/ImageResizer.cs +++ b/Composite/Core/WebClient/Media/ImageResizer.cs @@ -1,12 +1,15 @@ -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Linq; +using System.Security.Cryptography; +using System.Text; using System.Web; using System.Web.Caching; +using System.Web.Hosting; using Composite.Core.IO; using Composite.Data.Plugins.DataProvider.Streams; using Composite.Data.Types; @@ -14,87 +17,128 @@ namespace Composite.Core.WebClient.Media { /// - ///Class that performs image resizing + /// Class that performs image resizing /// /// [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public static class ImageResizer { private const string ResizedImagesCacheDirectory = "~/App_Data/Composite/Cache/Resized images"; - private static Dictionary _ImageFormat2Extension = new Dictionary - { - {ImageFormat.Png, "png"}, - {ImageFormat.Gif, "gif"}, - {ImageFormat.Jpeg, "jpg"}, - {ImageFormat.Bmp, "bmp"}, - {ImageFormat.Tiff, "tiff"} - }; - private static ImageCodecInfo JpegCodecInfo = ImageCodecInfo.GetImageEncoders().FirstOrDefault(codec => codec.FormatID == ImageFormat.Jpeg.Guid); + private static Dictionary _imageFormatProviders; + + private static Dictionary ImageFormatProviders + { + get + { + if (_imageFormatProviders != null) return _imageFormatProviders; + + var customProviders = ServiceLocator.GetServices().ToList(); + + var result = new Dictionary(); + customProviders.ForEach(provider => + { + var mediaType = provider.MediaType; + if (string.IsNullOrWhiteSpace(mediaType)) + throw new InvalidOperationException($"Empty MediaType returned by provider {provider.GetType().FullName}"); + + result[mediaType] = provider; + }); + + return _imageFormatProviders = result; + } + } private static readonly TimeSpan CacheExpirationTimeSpan = new TimeSpan(1, 0, 0, 0); private static string _resizedImagesDirectoryPath; + private static string ResizedImagesDirectoryPath + { + get + { + if (_resizedImagesDirectoryPath == null) + { + _resizedImagesDirectoryPath = HostingEnvironment.MapPath(ResizedImagesCacheDirectory); + + if (!C1Directory.Exists(_resizedImagesDirectoryPath)) + { + C1Directory.CreateDirectory(_resizedImagesDirectoryPath); + } + } + + return _resizedImagesDirectoryPath; + } + } /// /// Gets the resized image. /// - /// An instance of . /// The media file. /// The resizing options. - /// The target image format. + /// The media type of the image. + /// The media type for the resized image. /// A full file path to a resized image; null if there's no need to resize the image - public static string GetResizedImage(HttpServerUtility httpServerUtility, IMediaFile file, ResizingOptions resizingOptions, ImageFormat targetImageFormat) + public static string GetResizedImage(IMediaFile file, ResizingOptions resizingOptions, + string sourceMediaType, string targetMediaType) { - Verify.ArgumentNotNull(file, "file"); - Verify.That(ImageFormatIsSupported(targetImageFormat), "Unsupported image format '{0}'", targetImageFormat); + Verify.ArgumentNotNull(file, nameof(file)); + Verify.ArgumentNotNullOrEmpty(sourceMediaType, nameof(sourceMediaType)); + Verify.ArgumentNotNullOrEmpty(targetMediaType, nameof(targetMediaType)); - if (_resizedImagesDirectoryPath == null) - { - _resizedImagesDirectoryPath = httpServerUtility.MapPath(ResizedImagesCacheDirectory); + if (!ImageFormatProviders.TryGetValue(sourceMediaType, out var sourceImageFormatProvider)) + throw new ArgumentException($"Unsupported media type '{sourceMediaType}'", nameof(sourceMediaType)); - if (!C1Directory.Exists(_resizedImagesDirectoryPath)) - { - C1Directory.CreateDirectory(_resizedImagesDirectoryPath); - } - } + if (!ImageFormatProviders.TryGetValue(targetMediaType, out var imageFileFormatProvider)) + throw new ArgumentException($"Unsupported media type '{targetMediaType}'", nameof(targetMediaType)); - string imageKey = file.CompositePath; - bool isNativeProvider = file is FileSystemFileBase; + string imageKey = file.CompositePath; - string imageSizeCacheKey = "ShowMedia.ashx image size " + imageKey; + string imageSizeCacheKey = nameof(ImageResizer) + imageKey; Size? imageSize = HttpRuntime.Cache.Get(imageSizeCacheKey) as Size?; + Func loadImageFunc = sourceImageFormatProvider.LoadImageFromStream; + Bitmap bitmap = null; Stream fileStream = null; try { if (imageSize == null) { - fileStream = file.GetReadStream(); - - Size calculatedSize; - if (!ImageSizeReader.TryGetSize(fileStream, out calculatedSize)) + if (sourceImageFormatProvider.CanReadImageSize) { + fileStream = file.GetReadStream(); + if (sourceImageFormatProvider.TryGetSize(fileStream, out var imageSizeFromProvider)) + { + imageSize = imageSizeFromProvider; + } + + fileStream.Close(); fileStream.Dispose(); + fileStream = null; + } + + if (imageSize == null) + { fileStream = file.GetReadStream(); + bitmap = loadImageFunc(fileStream); - bitmap = new Bitmap(fileStream); - calculatedSize = new Size { Width = bitmap.Width, Height = bitmap.Height }; + imageSize = new Size { Width = bitmap.Width, Height = bitmap.Height }; } - imageSize = calculatedSize; // We can provider cache dependency only for the native media provider - var cacheDependency = isNativeProvider ? new CacheDependency((file as FileSystemFileBase).SystemPath) : null; + CacheDependency cacheDependency = null; + if (file is FileSystemFileBase fileSystemFile) + { + cacheDependency = new CacheDependency(fileSystemFile.SystemPath); + } HttpRuntime.Cache.Add(imageSizeCacheKey, imageSize, cacheDependency, DateTime.MaxValue, CacheExpirationTimeSpan, CacheItemPriority.Normal, null); } - int newWidth, newHeight; - bool centerCrop; - bool needToResize = CalculateSize(imageSize.Value.Width, imageSize.Value.Height, resizingOptions, out newWidth, out newHeight, out centerCrop); + bool needToResize = CalculateSize(imageSize.Value.Width, imageSize.Value.Height, resizingOptions, + out int newWidth, out int newHeight, out bool centerCrop); needToResize = needToResize || resizingOptions.CustomQuality; @@ -103,47 +147,55 @@ public static string GetResizedImage(HttpServerUtility httpServerUtility, IMedia return null; } - int filePathHash = imageKey.GetHashCode(); + string mediaFileHash = GetMediaHash(imageKey); string centerCroppedString = centerCrop ? "c" : string.Empty; - string fileExtension = _ImageFormat2Extension[targetImageFormat]; - string resizedImageFileName = string.Format("{0}x{1}_{2}{3}_{4}.{5}", newWidth, newHeight, filePathHash, centerCroppedString, resizingOptions.Quality, fileExtension); + string fileExtension = imageFileFormatProvider.FileExtension; + string qualityCacheKeyPart = imageFileFormatProvider.CanSetImageQuality + ? $"_{resizingOptions.Quality}" + : ""; - string imageFullPath = Path.Combine(_resizedImagesDirectoryPath, resizedImageFileName); + string resizedImageFileName = $"{newWidth}x{newHeight}{centerCroppedString}_{mediaFileHash}{qualityCacheKeyPart}.{fileExtension}"; - if (!C1File.Exists(imageFullPath) || C1File.GetLastWriteTime(imageFullPath) != file.LastWriteTime) + string resizedImageFullPath = Path.Combine(ResizedImagesDirectoryPath, resizedImageFileName); + + if (!C1File.Exists(resizedImageFullPath) || C1File.GetLastWriteTime(resizedImageFullPath) != file.LastWriteTime) { if (bitmap == null) { fileStream = file.GetReadStream(); - bitmap = new Bitmap(fileStream); + bitmap = loadImageFunc(fileStream); } - ResizeImage(bitmap, imageFullPath, newWidth, newHeight, centerCrop, targetImageFormat, resizingOptions.Quality); + using (Bitmap resizedImage = ResizeImage(bitmap, newWidth, newHeight, centerCrop)) + { + int? imageQuality = imageFileFormatProvider.CanSetImageQuality ? (int?)resizingOptions.Quality : null; + + imageFileFormatProvider.SaveImageToFile(resizedImage, resizedImageFullPath, imageQuality); + } if (file.LastWriteTime.HasValue) { - C1File.SetLastWriteTime(imageFullPath, file.LastWriteTime.Value); + C1File.SetLastWriteTime(resizedImageFullPath, file.LastWriteTime.Value); } } - return imageFullPath; + return resizedImageFullPath; } finally { - if (bitmap != null) - { - bitmap.Dispose(); - } - - if (fileStream != null) - { - fileStream.Dispose(); - } + bitmap?.Dispose(); + fileStream?.Dispose(); } } + private static string GetMediaHash(string mediaKeyPath) + { + var guid = HashingHelper.ComputeMD5Hash(mediaKeyPath, Encoding.UTF8); + return UrlUtils.CompressGuid(guid).Substring(10); + } + private static bool CalculateSize(int width, int height, ResizingOptions resizingOptions, out int newWidth, out int newHeight, out bool centerCrop) { // Can be refactored to use System.Drawing.Size class instead of (width & height). @@ -160,13 +212,13 @@ private static bool CalculateSize(int width, int height, ResizingOptions resizin centerCrop = false; - // If both height and width are defined - we have "scalling" + // If both height and width are defined - we have "scaling" if (resizingOptions.Height != null && resizingOptions.Width != null) { newHeight = (int)resizingOptions.Height; newWidth = (int)resizingOptions.Width; - // we do not allow scalling to a size, bigger than original one + // we do not allow scaling to a size, bigger than original one if (newHeight > height) { newHeight = height; @@ -226,7 +278,7 @@ private static bool CalculateSize(int width, int height, ResizingOptions resizin newWidth = width; newHeight = height; - // If image doesn't fit to bondaries "maxWidth X maxHeight", downsizing it + // If image doesn't fit to boundaries "maxWidth X maxHeight", downsizing it int? maxWidth = resizingOptions.Width; if (resizingOptions.MaxWidth != null && (maxWidth == null || resizingOptions.MaxWidth < maxWidth)) { @@ -256,27 +308,6 @@ private static bool CalculateSize(int width, int height, ResizingOptions resizin } - private static void ResizeImage(Bitmap image, string outputFilePath, int newWidth, int newHeight, bool centerCrop, ImageFormat imageFormat, int quality) - { - using (Bitmap resizedImage = ResizeImage(image, newWidth, newHeight, centerCrop)) - { - if (imageFormat.Guid == ImageFormat.Jpeg.Guid) - { - EncoderParameters parameters = new EncoderParameters(1); - - // Setting image quality, the deafult value is 75 - parameters.Param[0] = new EncoderParameter( - System.Drawing.Imaging.Encoder.Quality, quality); - - resizedImage.Save(outputFilePath, JpegCodecInfo, parameters); - } - else - { - resizedImage.Save(outputFilePath, imageFormat); - } - } - } - /// /// Resizes an image /// @@ -360,27 +391,26 @@ private static void DrawWithoutBlending(Graphics graphic, Bitmap bitmap, float x (int)Math.Ceiling(height - delta)); } - - - private static bool ImageFormatIsSupported(ImageFormat imageFormat) + /// + /// Returns a value indicating whether an image of a given media type can be resized. + /// + /// A media type. + public static bool SourceMediaTypeSupported(string mediaType) { - return _ImageFormat2Extension.ContainsKey(imageFormat); + if (mediaType == null) throw new ArgumentNullException(nameof(mediaType)); + + return ImageFormatProviders.ContainsKey(mediaType); } - /// - public class SupportedImageFormats + /// + /// Returns a value indicating whether resized imaged can be saved in the specified media type. + /// + /// A media type. + public static bool TargetMediaTypeSupported(string mediaType) { - /// - public static ImageFormat JPG { get { return ImageFormat.Jpeg; } } - /// - public static ImageFormat PNG { get { return ImageFormat.Png; } } - /// - public static ImageFormat TIFF { get { return ImageFormat.Tiff; } } - /// - public static ImageFormat GIF { get { return ImageFormat.Gif; } } - /// - public static ImageFormat BMP { get { return ImageFormat.Bmp; } } + if (mediaType == null) throw new ArgumentNullException(nameof(mediaType)); + return ImageFormatProviders.ContainsKey(mediaType); } } } diff --git a/Composite/Core/WebClient/Media/ResizingOptions.cs b/Composite/Core/WebClient/Media/ResizingOptions.cs index 6234ec176b..9a594531eb 100644 --- a/Composite/Core/WebClient/Media/ResizingOptions.cs +++ b/Composite/Core/WebClient/Media/ResizingOptions.cs @@ -1,7 +1,9 @@ -using System; +using System; using System.Collections.Specialized; +using System.Configuration; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Text; using System.Web; using System.Web.Caching; @@ -20,10 +22,14 @@ public class ResizingOptions private const string ResizedImageKeys = "~/App_Data/Composite/Media/ResizingOptions.xml"; private static string _resizedImageKeysFilePath; + private const string ResizingOptionsConfigFileName = "ImageResizing.xml"; + private static volatile string _hashSalt; + private static readonly object SyncRoot = new object(); + private int? _qualityOverride; /// - /// Image heigth + /// Image height /// public int? Height { get; set; } @@ -75,6 +81,12 @@ public int Quality /// public ResizingAction ResizingAction { get; set; } + /// + /// The preferred media type for the generated resized image. + /// If the media type isn't supported, the original image will be returned. + /// + public string MediaType { get; set; } + /// /// Indicates whether any options were specified /// @@ -126,6 +138,8 @@ internal ResizingOptions(string predefinedOptionsName) { ResizingAction = (ResizingAction)Enum.Parse(typeof(ResizingAction), attr.Value, true); } + + MediaType = (string) e.Attribute("mediaType"); } } @@ -185,6 +199,12 @@ private static ResizingOptions FromQueryString(NameValueCollection queryString) result.ResizingAction = ResizingAction.Stretch; } + var mediaType = queryString["mt"]; + if (!string.IsNullOrEmpty(mediaType)) + { + result.MediaType = mediaType; + } + return result; } private static XElement GetPredefinedResizingOptions() @@ -229,6 +249,32 @@ private static XElement GetPredefinedResizingOptions() return xel; } + private static string GetHashSalt() + { + if (_hashSalt != null) return _hashSalt; + + lock (SyncRoot) + { + if (_hashSalt != null) return _hashSalt; + + string filePath = Path.Combine(PathUtil.Resolve(GlobalSettingsFacade.ConfigurationDirectory), ResizingOptionsConfigFileName); + if (!C1File.Exists(filePath)) + { + var config = new XElement("ResizedImages", + new XAttribute("hashSalt", Guid.NewGuid())); + + config.SaveToPath(filePath); + } + + var xml = XElementUtils.Load(filePath); + + var hashSalt = (string)xml.Attribute("hashSalt"); + if (hashSalt == null) throw new ConfigurationErrorsException("Missing required attribute 'hashSalt'", null, filePath, 0); + + return _hashSalt = hashSalt; + } + } + /// public override string ToString() { @@ -253,8 +299,26 @@ public override string ToString() sb.Append("action=").Append(ResizingAction.ToString().ToLowerInvariant()); } + if (!string.IsNullOrWhiteSpace(MediaType)) + { + sb.Append(sb.Length == 0 ? String.Empty : "&"); + sb.Append("mt=").Append(MediaType); + } + return sb.ToString(); } + + /// + /// Gets a secure hash for the current resizing options. The hash is salted with machine key and can be used to confirm that + /// an image URL with resizing options was generated by the website. + /// + /// + public string GetSecureHash(Guid mediaId) + { + var value = mediaId + this.ToString() + GetHashSalt(); + var hash = HashingHelper.ComputeMD5Hash(value, Encoding.UTF8); + return hash.ToString().Substring(0, 5); + } } } diff --git a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs index 44f77c28b6..b3bf2beabc 100644 --- a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs +++ b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs @@ -258,7 +258,9 @@ internal static void ProcessXhtmlDocument(XhtmlDocument xhtmlDocument, IPage pag } } - private static bool IsMetaTag(XElement e) => e.Name.LocalName.Equals("meta", StringComparison.OrdinalIgnoreCase); + private static bool IsTitleOrMetaTag(XElement e) => + e.Name.LocalName.Equals("title", StringComparison.OrdinalIgnoreCase) + || e.Name.LocalName.Equals("meta", StringComparison.OrdinalIgnoreCase); private static bool CheckForDuplication(HashSet values, string value) { @@ -301,30 +303,43 @@ private static void RemoveDuplicates(XElement head) var priorityOrderedElements = new List(); - priorityOrderedElements.AddRange(head.Elements().Where(IsMetaTag)); + priorityOrderedElements.AddRange(head.Elements().Where(IsTitleOrMetaTag)); priorityOrderedElements.Reverse(); - priorityOrderedElements.AddRange(head.Elements().Where(e => !IsMetaTag(e))); + priorityOrderedElements.AddRange(head.Elements().Where(e => !IsTitleOrMetaTag(e))); + + bool titleTagEncountered = false; foreach (var e in priorityOrderedElements) { - var id = (string)e.Attribute(XName_Id); - - bool toBeRemoved = CheckForDuplication(uniqueIdValues, id); + var tagName = e.Name.LocalName.ToLowerInvariant(); + bool toBeRemoved; - if (!toBeRemoved && !e.Nodes().Any()) + if (tagName == "title") + { + toBeRemoved = titleTagEncountered; + titleTagEncountered = true; + } + else { - switch (e.Name.LocalName.ToLowerInvariant()) + var id = (string)e.Attribute(XName_Id); + + toBeRemoved = CheckForDuplication(uniqueIdValues, id); + + if (!toBeRemoved && !e.Nodes().Any()) { - case "meta": - var name = (string)e.Attribute(XName_Name); - toBeRemoved = CheckForDuplication(uniqueMetaNameValues, name); - break; - case "script": - toBeRemoved = CheckForDuplication(uniqueScriptAttributes, e.AttributesAsString()); - break; - case "link": - toBeRemoved = CheckForDuplication(uniqueLinkAttributes, e.AttributesAsString()); - break; + switch (tagName) + { + case "meta": + var name = (string)e.Attribute(XName_Name); + toBeRemoved = CheckForDuplication(uniqueMetaNameValues, name); + break; + case "script": + toBeRemoved = CheckForDuplication(uniqueScriptAttributes, e.AttributesAsString()); + break; + case "link": + toBeRemoved = CheckForDuplication(uniqueLinkAttributes, e.AttributesAsString()); + break; + } } } diff --git a/Composite/Data/DynamicTypes/DataTypeDescriptorFormsHelper.cs b/Composite/Data/DynamicTypes/DataTypeDescriptorFormsHelper.cs index f5dac54791..ab446f717a 100644 --- a/Composite/Data/DynamicTypes/DataTypeDescriptorFormsHelper.cs +++ b/Composite/Data/DynamicTypes/DataTypeDescriptorFormsHelper.cs @@ -15,6 +15,7 @@ using Composite.Data.ProcessControlled; using Composite.Data.ProcessControlled.ProcessControllers.GenericPublishProcessController; using Composite.Data.PublishScheduling; +using Composite.Data.Types; using Composite.Data.Validation; using Composite.Data.Validation.ClientValidationRules; using Composite.Functions; @@ -785,10 +786,10 @@ private void GenerateForm() } else { - if (_dataTypeDescriptor.SuperInterfaces.Contains(typeof(IPublishControlled))) + if (_showPublicationStatusSelector && _dataTypeDescriptor.SuperInterfaces.Contains(typeof(IPublishControlled))) { - fieldNameToBindingNameMapper.Add("PublishDate", "PublishDate"); - fieldNameToBindingNameMapper.Add("UnpublishDate", "UnpublishDate"); + fieldNameToBindingNameMapper.Add(nameof(IPublishSchedule.PublishDate), nameof(IPublishSchedule.PublishDate)); + fieldNameToBindingNameMapper.Add(nameof(IUnpublishSchedule.UnpublishDate), nameof(IUnpublishSchedule.UnpublishDate)); } Func> getBindingsFunc = diff --git a/Composite/Data/Types/IDataItemTreeAttachmentPoint.cs b/Composite/Data/Types/IDataItemTreeAttachmentPoint.cs index 215789aefe..2ee776f85b 100644 --- a/Composite/Data/Types/IDataItemTreeAttachmentPoint.cs +++ b/Composite/Data/Types/IDataItemTreeAttachmentPoint.cs @@ -1,47 +1,60 @@ -using System; +using System; +using Composite.C1Console.Elements.Plugins.ElementAttachingProvider; using Composite.Data.Hierarchy; using Composite.Data.Hierarchy.DataAncestorProviders; namespace Composite.Data.Types { - /// + /// + /// Represents a link between a tree definition and a data item. + /// F.e. a tree that shows navigation elements, attached to a specific C1 page. /// /// [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] [AutoUpdateble] [ImmutableTypeId("{24CDC117-4510-41C4-8A73-D1B4CD85FE2A}")] - [KeyPropertyName("Id")] + [KeyPropertyName(nameof(Id))] [DataAncestorProvider(typeof(NoAncestorDataAncestorProvider))] [DataScope(DataScopeIdentifier.PublicName)] - [CachingAttribute(CachingType.Full)] + [Caching(CachingType.Full)] public interface IDataItemTreeAttachmentPoint : IData { - /// + /// + /// The Id value for the attachment point. + /// [StoreFieldType(PhysicalStoreFieldType.Guid)] [ImmutableFieldId("{BEB44A8E-37FD-4FA6-A420-B252B8590AD6}")] Guid Id { get; set; } - /// + /// + /// The Id of the tree that is attached. + /// [StoreFieldType(PhysicalStoreFieldType.LargeString)] [ImmutableFieldId("{FA9A44E0-9D41-491A-86C0-BC5189FFC023}")] string TreeId { get; set; } - /// + /// + /// The position in which the tree elements should be shown, f.e. "Top" or "Bottom". See + /// [StoreFieldType(PhysicalStoreFieldType.String, 64)] [ImmutableFieldId("{6624A6CF-29DC-4E6A-8B88-25514BC00758}")] string Position { get; set; } - /// + /// + /// The data type of the data item, to which a tree is attached. + /// [StoreFieldType(PhysicalStoreFieldType.LargeString)] [ImmutableFieldId("{F08DB525-2218-4379-A6F6-A9A904DEF6FE}")] string InterfaceType { get; set; } - /// + /// + /// The key value of the data item to which a tree is attached. + /// [StoreFieldType(PhysicalStoreFieldType.LargeString)] [ImmutableFieldId("{8A50C365-586D-45F5-8881-EC3878E16593}")] string KeyValue { get; set; } diff --git a/Composite/Plugins/Data/DataProviders/FileSystemMediaFileProvider/FileSystemMediaFile.cs b/Composite/Plugins/Data/DataProviders/FileSystemMediaFileProvider/FileSystemMediaFile.cs index c96208687a..ce906172d3 100644 --- a/Composite/Plugins/Data/DataProviders/FileSystemMediaFileProvider/FileSystemMediaFile.cs +++ b/Composite/Plugins/Data/DataProviders/FileSystemMediaFileProvider/FileSystemMediaFile.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Globalization; using System.IO; using System.Security.Cryptography; using System.Text; +using Composite.Core; using Composite.Core.IO; using Composite.Data; using Composite.Data.Plugins.DataProvider.Streams; @@ -15,10 +16,6 @@ namespace Composite.Plugins.Data.DataProviders.FileSystemMediaFileProvider [FileStreamManager(typeof(FileSystemFileStreamManager))] internal sealed class FileSystemMediaFile : FileSystemFileBase, IMediaFile { - private static readonly MD5 HashingAlgorithm = MD5.Create(); - - - public FileSystemMediaFile(string systemPath, string fileName, string folderName, string storeId, DataSourceId dataSourceId) { Id = CalculateId(folderName, fileName); @@ -36,8 +33,7 @@ private static Guid CalculateId(string folderName, string fileName) private static Guid GetHashValue(string value) { - var bytes = HashingAlgorithm.ComputeHash(Encoding.ASCII.GetBytes(value)); - return new Guid(bytes); + return HashingHelper.ComputeMD5Hash(value, Encoding.ASCII); } diff --git a/Composite/Plugins/GlobalSettings/GlobalSettingsProviders/ConfigBasedGlobalSettingsProvider.cs b/Composite/Plugins/GlobalSettings/GlobalSettingsProviders/ConfigBasedGlobalSettingsProvider.cs index 5a2789d55f..d2c09b0e7f 100644 --- a/Composite/Plugins/GlobalSettings/GlobalSettingsProviders/ConfigBasedGlobalSettingsProvider.cs +++ b/Composite/Plugins/GlobalSettings/GlobalSettingsProviders/ConfigBasedGlobalSettingsProvider.cs @@ -103,6 +103,8 @@ public ConfigBasedGlobalSettingsProvider(ConfigBasedGlobalSettingsProviderData c _configurationData.InheritGlobalReadPermissionOnHiddenPerspectives; public bool OmitAspNetWebFormsSupport => _configurationData.OmitAspNetWebFormsSupport; + + public bool ProtectResizedImagesWithHash => _configurationData.ProtectResizedImagesWithHash; } internal class ConfigCachingSettings: ICachingSettings @@ -530,6 +532,14 @@ public bool OmitAspNetWebFormsSupport get { return (bool)base[_omitAspNetWebFormsSupportPropertyName]; } set { base[_omitAspNetWebFormsSupportPropertyName] = value; } } + + private const string _protectResizedImagesWithHash = "protectResizedImagesWithHash"; + [ConfigurationProperty(_protectResizedImagesWithHash, DefaultValue = false)] + public bool ProtectResizedImagesWithHash + { + get => (bool)base[_protectResizedImagesWithHash]; + set => base[_protectResizedImagesWithHash] = value; + } } diff --git a/Composite/Plugins/Logging/LogTraceListeners/FileLogTraceListener/FileLogger.cs b/Composite/Plugins/Logging/LogTraceListeners/FileLogTraceListener/FileLogger.cs index debff4ec2f..5229366b19 100644 --- a/Composite/Plugins/Logging/LogTraceListeners/FileLogTraceListener/FileLogger.cs +++ b/Composite/Plugins/Logging/LogTraceListeners/FileLogTraceListener/FileLogger.cs @@ -1,4 +1,4 @@ -//#define UseLockFiles +//#define UseLockFiles using System; using System.Collections.Generic; @@ -22,6 +22,9 @@ namespace Composite.Plugins.Logging.LogTraceListeners.FileLogTraceListener /// internal class FileLogger : IDisposable { + private const int MaxLogFiles = 10; + private bool _initializationFailed; + #if UseLockFiles private static readonly TimeSpan LockFileUpdateFrequency = TimeSpan.FromSeconds(20); private static readonly TimeSpan OldLockFilesPreservationTime = TimeSpan.FromSeconds(60); @@ -115,6 +118,8 @@ public DateTime StartupTime public void WriteEntry(LogEntry entry) { + if (_initializationFailed) return; + string logLine = entry.ToString(); byte[] bytes = Encoding.UTF8.GetBytes(logLine + "\n"); @@ -133,6 +138,7 @@ public void WriteEntry(LogEntry entry) } EnsureInitialize(); + if (_initializationFailed) return; FileConnection.NewEntries.Add(entry); @@ -326,7 +332,7 @@ private void EnsureInitialize() string fileNamePrefix = creationDate.ToString("yyyyMMdd"); - for (int i = 0; i < 10; i++) + for (int i = 0; i < MaxLogFiles; i++) { var fileName = fileNamePrefix + (i > 0 ? "_" + i : string.Empty) + ".txt"; string filePath = Path.Combine(_logDirectoryPath, fileName); @@ -377,7 +383,7 @@ private void EnsureInitialize() return; } - throw new InvalidOperationException("Failed to open/create a log file"); + _initializationFailed = true; } } diff --git a/Composite/Plugins/Routing/MediaUrlProviders/DefaultMediaUrlProvider.cs b/Composite/Plugins/Routing/MediaUrlProviders/DefaultMediaUrlProvider.cs index f29bf1e234..a84f3771c0 100644 --- a/Composite/Plugins/Routing/MediaUrlProviders/DefaultMediaUrlProvider.cs +++ b/Composite/Plugins/Routing/MediaUrlProviders/DefaultMediaUrlProvider.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.IO; using System.Text; using System.Web; using Composite.Core; +using Composite.Core.Configuration; using Composite.Core.IO; using Composite.Core.Routing; using Composite.Core.WebClient; @@ -83,7 +84,13 @@ public string GetResizedImageUrl(string storeId, Guid mediaId, ResizingOptions r if (resizingOptions != null && !resizingOptions.IsEmpty) { - return url + "?" + resizingOptions; + var urlWithResizing = url + "?" + resizingOptions; + if (!GlobalSettingsFacade.ProtectResizedImagesWithHash) + { + return urlWithResizing; + } + + return $"{urlWithResizing}&sh={resizingOptions.GetSecureHash(mediaId)}"; } return url.ToString(); diff --git a/Composite/Plugins/Search/Endpoint/ConsoleSearchRpcService.cs b/Composite/Plugins/Search/Endpoint/ConsoleSearchRpcService.cs index e8f51791f0..fc217b447b 100644 --- a/Composite/Plugins/Search/Endpoint/ConsoleSearchRpcService.cs +++ b/Composite/Plugins/Search/Endpoint/ConsoleSearchRpcService.cs @@ -130,7 +130,7 @@ public async Task QueryAsync(ConsoleSearchQuery query) var result = await _searchProvider.SearchAsync(searchQuery); - var items = result.Items.Evaluate(); + var items = result?.Items?.Evaluate() ?? Array.Empty(); if (!items.Any()) { return new ConsoleSearchResult diff --git a/Composite/Properties/SharedAssemblyInfo.cs b/Composite/Properties/SharedAssemblyInfo.cs index a6e496d5f4..ea5d1c1489 100644 --- a/Composite/Properties/SharedAssemblyInfo.cs +++ b/Composite/Properties/SharedAssemblyInfo.cs @@ -2,15 +2,15 @@ // General Information about the assemblies Composite and Composite.Workflows #if !InternalBuild -[assembly: AssemblyTitle("C1 CMS 6.10")] +[assembly: AssemblyTitle("C1 CMS 6.11")] #else -[assembly: AssemblyTitle("C1 CMS 6.10 (Internal Build)")] +[assembly: AssemblyTitle("C1 CMS 6.11 (Internal Build)")] #endif [assembly: AssemblyCompany("Orckestra Technologies Inc.")] [assembly: AssemblyProduct("C1 CMS")] -[assembly: AssemblyCopyright("Copyright © Orckestra Technologies Inc. 2020")] +[assembly: AssemblyCopyright("Copyright © Orckestra Technologies Inc. 2021")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -[assembly: AssemblyVersion("6.10.*")] +[assembly: AssemblyVersion("6.11.*")] diff --git a/Composite/Search/Crawling/SearchDocumentBuilder.cs b/Composite/Search/Crawling/SearchDocumentBuilder.cs index 6b82bffb0c..3898665a73 100644 --- a/Composite/Search/Crawling/SearchDocumentBuilder.cs +++ b/Composite/Search/Crawling/SearchDocumentBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; @@ -28,8 +28,6 @@ public class SearchDocumentBuilder private IPage _currentPage; - private static readonly MD5 HashingAlgorithm = MD5.Create(); - /// /// Creates a new instance of . /// @@ -67,6 +65,11 @@ public SearchDocumentBuilder(IEnumerable extens /// public string Url { get; set; } + /// + /// A factor by which a search document should be boosted index time. + /// + public float Boost { get; set; } = 1; + /// /// Sets the interface type, name of which will be used for populating the "Data Type" column in the search results. /// @@ -223,6 +226,7 @@ public SearchDocument BuildDocument( ElementBundleName = versionName, FullText = _textParts, Url = Url, + Boost = Boost, FieldValues = _fieldValues .ExcludeDuplicateKeys(pair => pair.Key) .ToDictionary(pair => pair.Key, pair => pair.Value), @@ -262,9 +266,9 @@ private void AddAccessField(EntityToken entityToken) internal static string GetEntityTokenHash(EntityToken entityToken) { var entityTokenString = EntityTokenSerializer.Serialize(entityToken); - var bytes = Encoding.UTF8.GetBytes(entityTokenString); + var md5Hash = HashingHelper.ComputeMD5Hash(entityTokenString, Encoding.UTF8); - return UrlUtils.CompressGuid(new Guid(HashingAlgorithm.ComputeHash(bytes))); + return UrlUtils.CompressGuid(md5Hash); } /// diff --git a/Composite/Search/Crawling/XhtmlCrawlingHelper.cs b/Composite/Search/Crawling/XhtmlCrawlingHelper.cs index b13d1a3540..31fbeab9ec 100644 --- a/Composite/Search/Crawling/XhtmlCrawlingHelper.cs +++ b/Composite/Search/Crawling/XhtmlCrawlingHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -105,7 +105,7 @@ private void ProcessElement(XElement element) return; } - if (element.Name.LocalName == "script" || element.Name.LocalName == "noscript") + if (element.Name.LocalName == "script" || element.Name.LocalName == "noscript" || element.Name.LocalName == "style") { return; } diff --git a/Composite/Search/SearchDocument.cs b/Composite/Search/SearchDocument.cs index 328186c733..ac23558fdf 100644 --- a/Composite/Search/SearchDocument.cs +++ b/Composite/Search/SearchDocument.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Composite.C1Console.Security; namespace Composite.Search @@ -97,5 +97,10 @@ public SearchDocument(string source, string id, string label, EntityToken entity /// Field values that is preserved in the index and will be used in faceted search. /// public IDictionary FacetFieldValues { get; set; } + + /// + /// A factor by which a search document should be boosted index time. + /// + public float Boost { get; set; } = 1; } } \ No newline at end of file diff --git a/Website/App_Data/Composite/DebugBuild.Composite.config b/Website/App_Data/Composite/DebugBuild.Composite.config index 95748ac093..fdab48af51 100644 --- a/Website/App_Data/Composite/DebugBuild.Composite.config +++ b/Website/App_Data/Composite/DebugBuild.Composite.config @@ -99,6 +99,7 @@ functionPreviewEnabled="true" inheritGlobalReadPermissionOnHiddenPerspectives="false" omitAspNetWebFormsSupport="false" + protectResizedImagesWithHash="true" > diff --git a/Website/App_Data/Composite/ReleaseBuild.Composite.config b/Website/App_Data/Composite/ReleaseBuild.Composite.config index 5889fa311b..22bc72222c 100644 --- a/Website/App_Data/Composite/ReleaseBuild.Composite.config +++ b/Website/App_Data/Composite/ReleaseBuild.Composite.config @@ -94,6 +94,7 @@ functionPreviewEnabled="true" inheritGlobalReadPermissionOnHiddenPerspectives="false" omitAspNetWebFormsSupport="false" + protectResizedImagesWithHash="true" > diff --git a/Website/Composite/localization/Composite.Plugins.TimezoneDisplayNames.en-us.xml b/Website/Composite/localization/Composite.Plugins.TimezoneDisplayNames.en-us.xml index 162c1aeb96..3f5d39f231 100644 --- a/Website/Composite/localization/Composite.Plugins.TimezoneDisplayNames.en-us.xml +++ b/Website/Composite/localization/Composite.Plugins.TimezoneDisplayNames.en-us.xml @@ -1,4 +1,4 @@ - + @@ -13,6 +13,7 @@ + @@ -38,6 +39,7 @@ + @@ -48,6 +50,7 @@ + @@ -65,30 +68,36 @@ + + + + + + diff --git a/Website/Composite/scripts/source/top/ui/bindings/data/DataBinding.js b/Website/Composite/scripts/source/top/ui/bindings/data/DataBinding.js index 98726597e2..1c4f373c79 100644 --- a/Website/Composite/scripts/source/top/ui/bindings/data/DataBinding.js +++ b/Website/Composite/scripts/source/top/ui/bindings/data/DataBinding.js @@ -70,7 +70,7 @@ DataBinding.expressions = { * Warnings. This is written *inside* the control, not in a balloon. Not all DataBindings * may support warnings; only bindings that rely on direct keyboard input. * @see {DataInputBinding} - * @see {TextBoxBinding} + * @see {TextBoxBinding} * TODO: Move to ConfigurationService? */ DataBinding.warnings = { @@ -85,13 +85,14 @@ DataBinding.warnings = { "maxlength" : "{0} characters maximum", "currency" : "Invalid notation", "email" : "Invalid e-mail", - "guid" : "Invalid GUID" + "guid" : "Invalid GUID", + "character" : "'{0}', hexadecimal value {1}, is an invalid character." } /** * Errors (balloons texts). All DataBindings support errors. * Remember that error presentation is handled by the FieldBinding. - * @see {FieldBinding#handleAction} + * @see {FieldBinding#handleAction} * TODO: Move to ConfigurationService? */ DataBinding.errors = { @@ -158,7 +159,7 @@ function DataBinding () { * The errortext associated, popularly known as balloons. * Remember that errors may also be injected by the server * in a special UpdatePanelBinding. - * @type {string} + * @type {string} */ this.error = null; diff --git a/Website/Composite/scripts/source/top/ui/bindings/data/keyboard/DataInputBinding.js b/Website/Composite/scripts/source/top/ui/bindings/data/keyboard/DataInputBinding.js index 4d1cfb54b8..3cf688889f 100644 --- a/Website/Composite/scripts/source/top/ui/bindings/data/keyboard/DataInputBinding.js +++ b/Website/Composite/scripts/source/top/ui/bindings/data/keyboard/DataInputBinding.js @@ -1,7 +1,10 @@ -DataInputBinding.prototype = new DataBinding; +DataInputBinding.prototype = new DataBinding; DataInputBinding.prototype.constructor = DataInputBinding; DataInputBinding.superclass = DataBinding.prototype; + +DataInputBinding.invalidXmlChar = /((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))/g; + /** * @class * @implements {IData} @@ -52,6 +55,18 @@ function DataInputBinding () { */ this._isInvalidBecauseRequired = false; + /** + * True when invalid because contains invalid Xml Char. + * @type {boolean} + */ + this._isInvalidBecauseInvalidXmlChar = false; + + /** + * The invalid Xml Char. + * @type {boolean} + */ + this._invalidXmlChar = null; + /** * True when invalid because of minlength. * @type {boolean} @@ -728,6 +743,11 @@ DataInputBinding.prototype.validate = function ( isInternal ) { if ( !this.isFocused ) { var message = null; + if ( this._isInvalidBecauseInvalidXmlChar == true && this._invalidXmlChar ) { + message = DataBinding.warnings [ "character" ]; + message = message.replace ( "{0}", String ( this._invalidXmlChar )); + message = message.replace ( "{1}", String ("0x" + this._invalidXmlChar.charCodeAt(0).toString(16) )); + } else if ( this._isInvalidBecauseRequired == true ) { message = DataBinding.warnings [ "required" ]; } else if ( this._isInvalidBecauseMinLength == true ) { @@ -776,6 +796,8 @@ DataInputBinding.prototype._normalizeToValid = function () { } }; + + /** * @return {boolean} */ @@ -783,11 +805,19 @@ DataInputBinding.prototype.isValid = function () { var isValid = true; this._isInvalidBecauseRequired = false; + this._isInvalidBecauseInvalidXmlChar = false; + this._invalidXmlChar = ""; this._isInvalidBecauseMinLength = false; this._isInvalidaBecuaseMaxLength = false; var value = this.getValue (); + var invalidCharMatch = DataInputBinding.invalidXmlChar.exec(value); + - if ( value == "" ) { + if (invalidCharMatch) { + isValid = false; + this._isInvalidBecauseInvalidXmlChar = true; + this._invalidXmlChar = invalidCharMatch[0]; + } else if ( value == "" ) { if ( this.isRequired == true ) { isValid = false; this._isInvalidBecauseRequired = true; @@ -950,6 +980,7 @@ DataInputBinding.prototype.updateElement = function ( element ) { return true; }; + /** * Manifest. Because postback without validation may happen, * we may need override validation message and post an empty diff --git a/Website/DebugBuild.Web.config b/Website/DebugBuild.Web.config index cc15d41f19..ba6144ea7c 100644 --- a/Website/DebugBuild.Web.config +++ b/Website/DebugBuild.Web.config @@ -29,7 +29,7 @@ - + diff --git a/Website/ReleaseBuild.Web.config b/Website/ReleaseBuild.Web.config index c271a9b782..1de84a9366 100644 --- a/Website/ReleaseBuild.Web.config +++ b/Website/ReleaseBuild.Web.config @@ -29,7 +29,7 @@ - + diff --git a/Website/Renderers/ShowMedia.ashx b/Website/Renderers/ShowMedia.ashx index 8014a415b7..d09930ff11 100644 --- a/Website/Renderers/ShowMedia.ashx +++ b/Website/Renderers/ShowMedia.ashx @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Drawing.Imaging; using System.Linq; using System.Text; using System.Web; @@ -14,6 +13,7 @@ using Composite.C1Console.Security; using Composite.Data; using Composite.Data.Types; using Composite.Core; +using Composite.Core.Configuration; using Composite.Core.IO; using Composite.Core.Extensions; using Composite.Core.WebClient; @@ -27,6 +27,15 @@ public class ShowMedia : IHttpHandler, IReadOnlySessionState private const int CopyBufferSize = 8192; private static readonly string MediaUrl_PublicPrefix = UrlUtils.PublicRootPath + "/media/"; + private static readonly string[] MediaTypesBrowserCanView = new[] + { + MimeTypeInfo.Flash, + MimeTypeInfo.Jpeg, + MimeTypeInfo.Png, + MimeTypeInfo.Gif, + MimeTypeInfo.Bmp + }; + private class Range { public Range(long offset, long length) @@ -174,15 +183,8 @@ public class ShowMedia : IHttpHandler, IReadOnlySessionState return false; } - private static void ValidateAndSend(HttpContext context, IMediaFile file) + private static void AddContentDispositionHeader(HttpContext context, IMediaFile file, string sourceMediaType, string resultMediaType) { - if (ExecuteResponseHandlers(context, file)) - { - return; - } - - context.Response.ContentType = GetMimeType(file); - string encodedFileName = file.FileName.Replace("\"", "_"); if (context.Request.Browser != null && context.Request.Browser.IsBrowser("ie")) { @@ -190,12 +192,40 @@ public class ShowMedia : IHttpHandler, IReadOnlySessionState encodedFileName = HttpUtility.UrlEncode(encodedFileName).Replace("+", "%20"); } + if (sourceMediaType != resultMediaType) + { + string originalExtension = ""; + + try + { + originalExtension = Path.GetExtension(encodedFileName); + if (originalExtension.StartsWith(".")) originalExtension = originalExtension.Substring(1); + } + catch {} + + var resultExtension = MimeTypeInfo.GetExtensionFromMimeType(resultMediaType); + + if(!string.IsNullOrEmpty(originalExtension) && !string.IsNullOrEmpty(resultExtension) && originalExtension != resultExtension + && encodedFileName.EndsWith("." + originalExtension)) + { + encodedFileName = encodedFileName.Substring(0, encodedFileName.Length - originalExtension.Length) + + resultExtension; + } + } + bool download = (string.IsNullOrEmpty(context.Request["download"]) ? - IsWebFormatImage(file) == false : + !CanBePreviewedInBrowser(context, file.MimeType) : context.Request["download"] != "false"); context.Response.AddHeader("Content-Disposition", "{0};filename=\"{1}\"".FormatWith((download ? "attachment" : "inline"), encodedFileName)); + } + private static void ValidateAndSend(HttpContext context, IMediaFile file) + { + if (ExecuteResponseHandlers(context, file)) + { + return; + } bool checkIfModifiedSince = false; @@ -220,16 +250,23 @@ public class ShowMedia : IHttpHandler, IReadOnlySessionState { FileOrStream source; - if (file.MimeType == "image/jpeg" || file.MimeType == "image/gif" || file.MimeType == "image/png" - || file.MimeType == "image/bmp" || file.MimeType == "image/tiff") + string mediaType = GetMediaType(file); + string outputMediaType; + + if (ImageResizer.SourceMediaTypeSupported(mediaType)) { - source = ProcessImageResizing(context, file); + source = ProcessImageResizing(context, file, mediaType, out outputMediaType); } else { source = new FileOrStream(file); + outputMediaType = mediaType; } + context.Response.ContentType = outputMediaType; + + AddContentDispositionHeader(context, file, mediaType, outputMediaType); + long? length = null; bool canSeek; @@ -428,7 +465,7 @@ public class ShowMedia : IHttpHandler, IReadOnlySessionState if (beginOffset == null) { - Verify.That(endOffset <= streamLength, "The segment is bigger than the lenght of the file"); + Verify.That(endOffset <= streamLength, "The segment is bigger than the length of the file"); result.Add(new Range(streamLength - endOffset.Value, endOffset.Value)); continue; @@ -490,86 +527,80 @@ public class ShowMedia : IHttpHandler, IReadOnlySessionState return null; } - private static string GetMimeType(IMediaFile file) + private static string GetMediaType(IMediaFile file) { - string mimeType = file.MimeType; + string mediaType = file.MimeType; - if (mimeType == MimeTypeInfo.Default) + if (mediaType == MimeTypeInfo.Default) { - mimeType = MimeTypeInfo.GetCanonicalFromExtension(Path.GetExtension(file.FileName.ToLowerInvariant())); + mediaType = MimeTypeInfo.GetCanonicalFromExtension(Path.GetExtension(file.FileName.ToLowerInvariant())); } - return mimeType; + return mediaType; } - private static bool IsWebFormatImage(IMediaFile file) + private static bool CanBePreviewedInBrowser(HttpContext context, string mediaType) { - switch (file.MimeType) - { - case "application/x-shockwave-flash": - case "image/png": - case "image/gif": - case "image/jpeg": - return true; - default: - return false; - } + return MediaTypesBrowserCanView.Contains(mediaType) || context.Request.AcceptTypes.Contains(mediaType); } + public bool IsReusable { get { return true; } } - public bool IsReusable + private static string GetResizedImageMediaType(string mediaType) { - get + if (mediaType == MimeTypeInfo.Gif) + { + // Returning image in PNG format because build-in GIF encoder produces images of a bad quality + return MimeTypeInfo.Png; + } + + if (mediaType == MimeTypeInfo.Bmp) { - return false; + return MimeTypeInfo.Jpeg; } + + if (ImageResizer.TargetMediaTypeSupported(mediaType)) + { + return mediaType; + } + + // Converting resized images to jpeg by default + return MimeTypeInfo.Jpeg; } - private static FileOrStream ProcessImageResizing(HttpContext context, IMediaFile file) + private static FileOrStream ProcessImageResizing(HttpContext context, IMediaFile file, string mediaType, out string resizedImageMediaType) { var resizingOptions = ResizingOptions.Parse(context.Request.QueryString); - if (resizingOptions == null || resizingOptions.IsEmpty) + var preferredMediaType = resizingOptions.MediaType; + + if (resizingOptions.IsEmpty + || (preferredMediaType != null && !ImageResizer.TargetMediaTypeSupported(preferredMediaType))) { + resizedImageMediaType = mediaType; return new FileOrStream(file); } - //Determine the content type, and save - //what image type we have for later use - - ImageFormat imgType; - if (file.MimeType == "image/jpeg") - { - imgType = ImageResizer.SupportedImageFormats.JPG; - } - else if (file.MimeType == "image/gif") + if (GlobalSettingsFacade.ProtectResizedImagesWithHash && !UserValidationFacade.IsLoggedIn()) { - // Returning image in PNG format because build-in GIF encoder produces images of a bad quality - imgType = ImageResizer.SupportedImageFormats.PNG; - } - else if (file.MimeType == "image/png") - { - imgType = ImageResizer.SupportedImageFormats.PNG; - } - else if (file.MimeType == "image/tiff") - { - imgType = ImageResizer.SupportedImageFormats.TIFF; - } - else if (file.MimeType == "image/bmp") - { - imgType = ImageResizer.SupportedImageFormats.BMP; - } - else - { - // Converting resized images to jpeg by default - imgType = ImageFormat.Jpeg; + var expectedHash = resizingOptions.GetSecureHash(file.Id); + if (context.Request.QueryString["sh"] != expectedHash) + { + // Returning the media file without resizing + resizedImageMediaType = mediaType; + return new FileOrStream(file); + } } + var targetImageMediaType = GetResizedImageMediaType(preferredMediaType ?? mediaType); + try { - string resizedImageFilePath = ImageResizer.GetResizedImage(context.Server, file, resizingOptions, imgType); + string resizedImageFilePath = ImageResizer.GetResizedImage(file, resizingOptions, mediaType, targetImageMediaType); + + resizedImageMediaType = resizedImageFilePath != null ? targetImageMediaType : mediaType; return resizedImageFilePath != null ? new FileOrStream(resizedImageFilePath) @@ -579,6 +610,8 @@ public class ShowMedia : IHttpHandler, IReadOnlySessionState { Log.LogVerbose("Composite.Media.ImageResize", ex.Message); } + + resizedImageMediaType = mediaType; return new FileOrStream(file); } diff --git a/Website/WebSite.csproj b/Website/WebSite.csproj index c0eb6cac51..48fd1e830c 100644 --- a/Website/WebSite.csproj +++ b/Website/WebSite.csproj @@ -11,7 +11,7 @@ Library Composite Composite.Website - v4.7.1 + v4.8 SAK SAK SAK @@ -2805,7 +2805,7 @@ ) if not exist "$(TargetDir)\System.Threading.Tasks.Dataflow.dll" ( - copy "$(ProjectDir)\..\Packages\System.Threading.Tasks.Dataflow.4.6.0\lib\netstandard1.1\System.Threading.Tasks.Dataflow.dll" "$(TargetDir)\System.Threading.Tasks.Dataflow.dll" + copy "$(ProjectDir)\..\Packages\System.Threading.Tasks.Dataflow.4.7.0\lib\netstandard1.1\System.Threading.Tasks.Dataflow.dll" "$(TargetDir)\System.Threading.Tasks.Dataflow.dll" ) cd "$(ProjectDir)" diff --git a/Website/test/e2e/reset.js b/Website/test/e2e/reset.js index 81118073a9..7ca2fd39cf 100644 --- a/Website/test/e2e/reset.js +++ b/Website/test/e2e/reset.js @@ -94,6 +94,7 @@ // 'bin/Microsoft.Web.*.dll', 'bin/Microsoft.WindowsAzure.*.dll', 'bin/Orckestra.Search*.dll', + 'bin/Orckestra.Web.BundlingAndMinification.dll', 'bin/Orckestra.Web.Css.Less.dll', 'bin/System.Web.Optimization.dll', 'bin/WebGrease.dll',