@@ -341,8 +357,10 @@ internal static Type FindIEnumerable(this Type seqType)
{
if (seqType == null || seqType == typeof(string))
return null;
+
if (seqType.IsArray)
return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType());
+
if (seqType.IsGenericType)
{
var args = seqType.GetGenericArguments();
@@ -353,6 +371,7 @@ internal static Type FindIEnumerable(this Type seqType)
return ienum;
}
}
+
Type[] ifaces = seqType.GetInterfaces();
if (ifaces.Length > 0)
{
@@ -363,8 +382,10 @@ internal static Type FindIEnumerable(this Type seqType)
return ienum;
}
}
+
if (seqType.BaseType != null && seqType.BaseType != typeof(object))
return FindIEnumerable(seqType.BaseType);
+
return null;
}
}
diff --git a/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs b/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs
index b223e44a64..5238183265 100644
--- a/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs
+++ b/src/Libraries/SmartStore.Core/Fakes/FakeHttpRequest.cs
@@ -136,31 +136,11 @@ public override string UserHostAddress
get { return null; }
}
- public override string RawUrl
- {
- get { return this.ApplicationPath; }
- }
-
- public override bool IsSecureConnection
- {
- get { return false; }
- }
-
- public override bool IsAuthenticated
- {
- get
- {
- return false;
- }
- }
-
- public override string[] UserLanguages
- {
- get
- {
- return new string[] { };
- }
- }
+ public override string RawUrl => this.ApplicationPath;
+ public override bool IsSecureConnection => false;
+ public override bool IsAuthenticated => false;
+ public override string[] UserLanguages => new string[] { };
+ public override string UserAgent => "SmartStore.NET";
public override RequestContext RequestContext
{
diff --git a/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs b/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs
index 6f03a2da49..98e482d393 100644
--- a/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs
+++ b/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs
@@ -3,23 +3,21 @@
using System.Text.RegularExpressions;
using System.Web;
using System.Linq;
+using AngleSharp;
+using AngleSharp.Parser.Html;
+using AngleSharp.Parser.Css;
namespace SmartStore.Core.Html
{
///
- /// Represents a HTML helper
+ /// Utility class for html manipulation or creation
///
public partial class HtmlUtils
{
- #region Fields
- private readonly static Regex paragraphStartRegex = new Regex("", RegexOptions.IgnoreCase);
- private readonly static Regex paragraphEndRegex = new Regex("
", RegexOptions.IgnoreCase);
+ private readonly static Regex _paragraphStartRegex = new Regex("", RegexOptions.IgnoreCase);
+ private readonly static Regex _paragraphEndRegex = new Regex("
", RegexOptions.IgnoreCase);
//private static Regex ampRegex = new Regex("&(?!(?:#[0-9]{2,4};|[a-z0-9]+;))", RegexOptions.Compiled | RegexOptions.IgnoreCase);
- #endregion
-
- #region Utilities
-
private static string EnsureOnlyAllowedHtml(string text)
{
if (String.IsNullOrEmpty(text))
@@ -56,9 +54,7 @@ private static bool IsValidTag(string tag, string tags)
return allowedTags.Any(aTag => tag == aTag);
}
- #endregion
- #region Methods
///
/// Formats the text
///
@@ -70,9 +66,14 @@ private static bool IsValidTag(string tag, string tags)
/// A value indicating whether to resolve links
/// A value indicating whether to add "noFollow" tag
/// Formatted text
- public static string FormatText(string text, bool stripTags,
- bool convertPlainTextToHtml, bool allowHtml,
- bool allowBBCode, bool resolveLinks, bool addNoFollowTag)
+ public static string FormatText(
+ string text,
+ bool stripTags,
+ bool convertPlainTextToHtml,
+ bool allowHtml,
+ bool allowBBCode,
+ bool resolveLinks,
+ bool addNoFollowTag)
{
if (String.IsNullOrEmpty(text))
@@ -178,8 +179,7 @@ public static string ConvertPlainTextToHtml(string text)
/// A value indicating whether to decode text
/// A value indicating whether to replace anchor text (remove a tag from the following url Name and output only the string "Name")
/// Formatted text
- public static string ConvertHtmlToPlainText(string text,
- bool decode = false, bool replaceAnchorTags = false)
+ public static string ConvertHtmlToPlainText(string text, bool decode = false, bool replaceAnchorTags = false)
{
if (String.IsNullOrEmpty(text))
return string.Empty;
@@ -257,8 +257,8 @@ public static string ConvertPlainTextToParagraph(string text)
if (String.IsNullOrEmpty(text))
return string.Empty;
- text = paragraphStartRegex.Replace(text, string.Empty);
- text = paragraphEndRegex.Replace(text, "\n");
+ text = _paragraphStartRegex.Replace(text, string.Empty);
+ text = _paragraphEndRegex.Replace(text, "\n");
text = text.Replace("\r\n", "\n").Replace("\r", "\n");
text = text + "\n\n";
text = text.Replace("\n\n", "\n");
@@ -271,8 +271,39 @@ public static string ConvertPlainTextToParagraph(string text)
builder.AppendFormat("{0}
\n", str);
}
}
+
return builder.ToString();
}
- #endregion
+
+ ///
+ /// Converts all occurences of pixel-based inline font-size expression to relative 'em'
+ ///
+ ///
+ ///
+ ///
+ public static string RelativizeFontSizes(string html, int baseFontSizePx = 16)
+ {
+ Guard.NotEmpty(html, nameof(html));
+ Guard.IsPositive(baseFontSizePx, nameof(baseFontSizePx));
+
+ var parser = new HtmlParser(new AngleSharp.Configuration().WithCss());
+ var doc = parser.Parse(html);
+
+ var nodes = doc.QuerySelectorAll("*[style]");
+ foreach (var node in nodes)
+ {
+ if (node.Style.FontSize is string s && s.EndsWith("px"))
+ {
+ var size = s.Substring(0, s.Length - 2).Convert();
+ if (size > 0)
+ {
+ //node.Style.FontSize = Math.Round(((double)size / (double)baseFontSizePx), 4) + "em";
+ node.Style.FontSize = "{0}em".FormatInvariant(Math.Round(((double)size / (double)baseFontSizePx), 4));
+ }
+ }
+ }
+
+ return doc.Body.InnerHtml;
+ }
}
}
diff --git a/src/Libraries/SmartStore.Core/IMergedData.cs b/src/Libraries/SmartStore.Core/IMergedData.cs
index fa1b2e61b5..8233a55987 100644
--- a/src/Libraries/SmartStore.Core/IMergedData.cs
+++ b/src/Libraries/SmartStore.Core/IMergedData.cs
@@ -1,4 +1,7 @@
using System.Collections.Generic;
+using System.Web.Hosting;
+using SmartStore.Core.Data;
+using SmartStore.Core.Infrastructure;
namespace SmartStore.Core
{
@@ -7,4 +10,38 @@ public interface IMergedData
bool MergedDataIgnore { get; set; }
Dictionary MergedDataValues { get; }
}
+
+ public static class IMergedDataExtensions
+ {
+ public static T GetMergedDataValue(this IMergedData mergedData, string key, T defaultValue)
+ {
+ if (mergedData.MergedDataValues == null)
+ return defaultValue;
+
+ if (mergedData.MergedDataIgnore)
+ return defaultValue;
+
+ if (mergedData is BaseEntity && HostingEnvironment.IsHosted)
+ {
+ // This is absolutely bad coding! But I don't see any alternatives.
+ // When the passed object is a (EF)-trackable entity,
+ // we cannot return the merged value while EF performs
+ // change detection, because entity properties could be set to modified,
+ // where in fact nothing has changed.
+ var dbContext = EngineContext.Current.Resolve();
+ if (dbContext.IsDetectingChanges())
+ {
+ return defaultValue;
+ }
+ }
+
+ object value;
+ if (mergedData.MergedDataValues.TryGetValue(key, out value))
+ {
+ return (T)value;
+ }
+
+ return defaultValue;
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/IO/IFile.cs b/src/Libraries/SmartStore.Core/IO/IFile.cs
index 53f86533eb..6d86299fc8 100644
--- a/src/Libraries/SmartStore.Core/IO/IFile.cs
+++ b/src/Libraries/SmartStore.Core/IO/IFile.cs
@@ -1,4 +1,5 @@
using System;
+using System.Drawing;
using System.IO;
using System.Threading.Tasks;
@@ -6,11 +7,46 @@ namespace SmartStore.Core.IO
{
public interface IFile
{
- string Path { get; }
- string Name { get; }
+ ///
+ /// The path relative to the storage root
+ ///
+ string Path { get; }
+
+ ///
+ /// The path without the file part, but with trailing slash
+ ///
+ string Directory { get; }
+
+ ///
+ /// File name including extension
+ ///
+ string Name { get; }
+
+ ///
+ /// File name excluding extension
+ ///
+ string Title { get; }
+
+ ///
+ /// Size in bytes
+ ///
long Size { get; }
+
+ ///
+ /// Expressed as UTC time
+ ///
DateTime LastUpdated { get; }
- string FileType { get; }
+
+ ///
+ /// File extension including dot
+ ///
+ string Extension { get; }
+
+ ///
+ /// Dimensions, if the file is an image.
+ ///
+ Size Dimensions { get; }
+
bool Exists { get; }
///
diff --git a/src/Libraries/SmartStore.Core/IO/IFileSystem.cs b/src/Libraries/SmartStore.Core/IO/IFileSystem.cs
index da424d28a7..c90b886ffb 100644
--- a/src/Libraries/SmartStore.Core/IO/IFileSystem.cs
+++ b/src/Libraries/SmartStore.Core/IO/IFileSystem.cs
@@ -7,6 +7,11 @@ namespace SmartStore.Core.IO
{
public interface IFileSystem
{
+ ///
+ /// Checks whether the underlying storage is remote, like 'Azure' for example.
+ ///
+ bool IsCloudStorage { get; }
+
///
/// Gets the root path
///
@@ -16,8 +21,12 @@ public interface IFileSystem
/// Retrieves the public URL for a given file within the storage provider.
///
/// The relative path within the storage provider.
+ ///
+ /// If true and the storage is in the cloud, returns the actual remote cloud URL to the resource.
+ /// If false, retrieves an app relative URL to delegate further processing to the media middleware (which can handle remote files)
+ ///
/// The public URL.
- string GetPublicUrl(string path);
+ string GetPublicUrl(string path, bool forCloud = false);
///
/// Retrieves the path within the storage provider for a given public url.
@@ -64,12 +73,24 @@ public interface IFileSystem
/// If the file or the folder is not found.
IFolder GetFolderForFile(string path);
+ ///
+ /// Retrieves the count of files within a path.
+ ///
+ /// The relative path to the folder in which to retrieve file count.
+ /// The file pattern to match
+ /// Optional. Files matching the predicate are excluded.
+ /// Whether to count files in all subfolders also
+ /// Total count of files.
+ long CountFiles(string path, string pattern, Func predicate, bool deep = true);
+
///
/// Performs a deep search for files within a path.
///
/// The relative path to the folder in which to process file search.
+ /// The file pattern to match
+ /// Whether to search in all subfolders also
/// Matching file names
- IEnumerable SearchFiles(string path, string pattern);
+ IEnumerable SearchFiles(string path, string pattern, bool deep = true);
///
/// Lists the files within a storage provider's path.
diff --git a/src/Libraries/SmartStore.Core/IO/IFileSystemExtensions.cs b/src/Libraries/SmartStore.Core/IO/IFileSystemExtensions.cs
index 673dab05dd..755aee74e1 100644
--- a/src/Libraries/SmartStore.Core/IO/IFileSystemExtensions.cs
+++ b/src/Libraries/SmartStore.Core/IO/IFileSystemExtensions.cs
@@ -222,5 +222,68 @@ public static bool TryCreateFolder(this IFileSystem fileSystem, string path)
return true;
}
+ ///
+ /// Checks whether the name of the file is unique within its directory.
+ /// When given file exists, this method appends [1...n] to the file title until
+ /// the check returns false.
+ ///
+ /// The path of file to check
+ /// An object containing the unique file's info, or null if method returns false
+ ///
+ /// false when does not exist yet. true otherwise.
+ ///
+ public static bool CheckFileUniqueness(this IFileSystem fileSystem, string path, out IFile uniqueFile)
+ {
+ Guard.NotEmpty(path, nameof(path));
+
+ uniqueFile = null;
+
+ var file = fileSystem.GetFile(path);
+ if (!file.Exists)
+ {
+ return false;
+ }
+
+ var pattern = string.Concat(file.Title, "-*", file.Extension);
+ var dir = file.Directory;
+ var files = new HashSet(fileSystem.SearchFiles(dir, pattern, false).Select(x => Path.GetFileName(x)));
+
+ int i = 1;
+ while (true)
+ {
+ var newFileName = string.Concat(file.Title, "-", i, file.Extension);
+ if (!files.Contains(newFileName))
+ {
+ // Found our gap
+ uniqueFile = fileSystem.GetFile(string.Concat(dir, newFileName));
+ return true;
+ }
+
+ i++;
+ }
+ }
+
+ ///
+ /// Retrieves the count of files within a path.
+ ///
+ /// The relative path to the folder in which to retrieve file count.
+ /// The file pattern to match
+ /// Whether to count files in all subfolders also
+ /// Total count of files.
+ public static long CountFiles(this IFileSystem fileSystem, string path, string pattern, bool deep = true)
+ {
+ return fileSystem.CountFiles(path, pattern, null, deep);
+ }
+
+ ///
+ /// Retrieves the count of files within a path.
+ ///
+ /// The relative path to the folder in which to retrieve file count.
+ /// Whether to count files in all subfolders also
+ /// Total count of files.
+ public static long CountFiles(this IFileSystem fileSystem, string path, bool deep = true)
+ {
+ return fileSystem.CountFiles(path, "*", null, deep);
+ }
}
}
diff --git a/src/Libraries/SmartStore.Core/IO/ImageHeader.cs b/src/Libraries/SmartStore.Core/IO/ImageHeader.cs
new file mode 100644
index 0000000000..c359438b7d
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/IO/ImageHeader.cs
@@ -0,0 +1,373 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace SmartStore.Core.IO
+{
+ ///
+ /// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349
+ /// Minor improvements including supporting unsigned 16-bit integers when decoding Jfif and added logic
+ /// to load the image using new Bitmap if reading the headers fails
+ ///
+ public static class ImageHeader
+ {
+ internal class UnknownImageFormatException : ArgumentException
+ {
+ public UnknownImageFormatException(string paramName = "", Exception e = null)
+ : base("Could not recognise image format.", paramName, e)
+ {
+ }
+ }
+
+ private static Dictionary> _imageFormatDecoders = new Dictionary>()
+ {
+ { new byte[] { 0x42, 0x4D }, DecodeBitmap },
+ { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif },
+ { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif },
+ { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng },
+ // { new byte[] { 0xff, 0xd8 }, DecodeJfif },
+ //{ new byte[] { 0xff, 0xd8, 0xff, 0xe0 }, DecodeJpeg },
+ //{ new byte[] { 0xff }, DecodeJpeg2 },
+ };
+
+ private static int _maxMagicBytesLength = 0;
+
+ static ImageHeader()
+ {
+ _maxMagicBytesLength = _imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length;
+ }
+
+ ///
+ /// Gets the dimensions of an image.
+ ///
+ /// The path of the image to get the dimensions for.
+ /// The dimensions of the specified image.
+ public static Size GetDimensions(string path)
+ {
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException("File '{0}' does not exist.".FormatInvariant(path));
+ }
+
+ var mime = MimeTypes.MapNameToMimeType(path);
+ return GetDimensions(File.OpenRead(path), mime, false);
+ }
+
+ ///
+ /// Gets the dimensions of an image.
+ ///
+ /// The bytes of the image to get the dimensions for.
+ /// The MIME type of the image. Can be null.
+ /// The dimensions of the specified image.
+ public static Size GetDimensions(byte[] buffer, string mime = null)
+ {
+ if (buffer == null || buffer.Length == 0)
+ {
+ return Size.Empty;
+ }
+
+ return GetDimensions(new MemoryStream(buffer), mime, false);
+ }
+
+ ///
+ /// Gets the dimensions of an image.
+ ///
+ /// The stream of the image to get the dimensions for.
+ /// If false, the passed stream will get disposed
+ /// The dimensions of the specified image.
+ public static Size GetDimensions(Stream input, bool leaveOpen = true)
+ {
+ return GetDimensions(input, null, leaveOpen);
+ }
+
+ ///
+ /// Gets the dimensions of an image.
+ ///
+ /// The stream of the image to get the dimensions for.
+ /// The MIME type of the image. Can be null.
+ /// If false, the passed stream will get disposed
+ /// The dimensions of the specified image.
+ public static Size GetDimensions(Stream input, string mime, bool leaveOpen = true)
+ {
+ Guard.NotNull(input, nameof(input));
+
+ var gdip = false;
+
+ if (!input.CanSeek || input.Length == 0)
+ {
+ return Size.Empty;
+ }
+
+ try
+ {
+ if (mime == "image/jpeg")
+ {
+ // Reading JPEG header does not work reliably
+ gdip = true;
+ return GetDimensionsByGdip(input);
+ }
+
+ using (var reader = new BinaryReader(input, Encoding.Unicode, true))
+ {
+ return GetDimensions(reader);
+ }
+ }
+ catch (Exception ex)
+ {
+ if (gdip)
+ {
+ throw ex;
+ }
+
+ // something went wrong with fast image access,
+ // so get original size the classic way
+ try
+ {
+ input.Seek(0, SeekOrigin.Begin);
+ return GetDimensionsByGdip(input);
+ }
+ catch
+ {
+ throw ex;
+ }
+ }
+ finally
+ {
+ if (!leaveOpen)
+ {
+ input.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Gets the dimensions of an image.
+ ///
+ /// The path of the image to get the dimensions of.
+ /// The dimensions of the specified image.
+ /// The image was of an unrecognised format.
+ public static Size GetDimensions(BinaryReader binaryReader)
+ {
+ byte[] magicBytes = new byte[_maxMagicBytesLength];
+ for (int i = 0; i < _maxMagicBytesLength; i += 1)
+ {
+ magicBytes[i] = binaryReader.ReadByte();
+ foreach (var kvPair in _imageFormatDecoders)
+ {
+ if (StartsWith(magicBytes, kvPair.Key))
+ {
+ return kvPair.Value(binaryReader);
+ }
+ }
+ }
+
+ throw new UnknownImageFormatException("binaryReader");
+ }
+
+ private static Size GetDimensionsByGdip(Stream input)
+ {
+ using (var b = Image.FromStream(input, false, false))
+ {
+ return new Size(b.Width, b.Height);
+ }
+ }
+
+ private static bool StartsWith(byte[] thisBytes, byte[] thatBytes)
+ {
+ for (int i = 0; i < thatBytes.Length; i += 1)
+ {
+ if (thisBytes[i] != thatBytes[i])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static short ReadLittleEndianInt16(BinaryReader binaryReader)
+ {
+ byte[] bytes = new byte[sizeof(short)];
+
+ for (int i = 0; i < sizeof(short); i += 1)
+ {
+ bytes[sizeof(short) - 1 - i] = binaryReader.ReadByte();
+ }
+ return BitConverter.ToInt16(bytes, 0);
+ }
+
+ private static ushort ReadLittleEndianUInt16(BinaryReader binaryReader)
+ {
+ byte[] bytes = new byte[sizeof(ushort)];
+
+ for (int i = 0; i < sizeof(ushort); i += 1)
+ {
+ bytes[sizeof(ushort) - 1 - i] = binaryReader.ReadByte();
+ }
+ return BitConverter.ToUInt16(bytes, 0);
+ }
+
+ private static int ReadLittleEndianInt32(BinaryReader binaryReader)
+ {
+ byte[] bytes = new byte[sizeof(int)];
+ for (int i = 0; i < sizeof(int); i += 1)
+ {
+ bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte();
+ }
+ return BitConverter.ToInt32(bytes, 0);
+ }
+
+ private static Size DecodeBitmap(BinaryReader binaryReader)
+ {
+ binaryReader.ReadBytes(16);
+ int width = binaryReader.ReadInt32();
+ int height = binaryReader.ReadInt32();
+ return new Size(width, height);
+ }
+
+ private static Size DecodeGif(BinaryReader binaryReader)
+ {
+ int width = binaryReader.ReadInt16();
+ int height = binaryReader.ReadInt16();
+ return new Size(width, height);
+ }
+
+ private static Size DecodePng(BinaryReader binaryReader)
+ {
+ binaryReader.ReadBytes(8);
+ int width = ReadLittleEndianInt32(binaryReader);
+ int height = ReadLittleEndianInt32(binaryReader);
+ return new Size(width, height);
+ }
+
+ #region Experiments
+
+ private static Size DecodeJpeg(BinaryReader reader)
+ {
+ // For JPEGs, we need to read the first 12 bytes of each chunk.
+ // We'll read those 12 bytes at buf+2...buf+14, i.e. overwriting the existing buf.
+
+ var buf = (new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }).Concat(reader.ReadBytes(20)).ToArray();
+
+ using (var f = new MemoryStream(buf))
+ {
+ if (buf[6] == (byte)'J' && buf[7] == (byte)'F' && buf[8] == (byte)'I' && buf[9] == (byte)'F')
+ {
+ var len = buf.Length;
+ long pos = 2;
+ while (buf[2] == 0xFF)
+ {
+ if (buf[3] == 0xC0 || buf[3] == 0xC1 || buf[3] == 0xC2 || buf[3] == 0xC3 || buf[3] == 0xC9 || buf[3] == 0xCA || buf[3] == 0xCB) break;
+ pos += 2 + (buf[4] << 8) + buf[5];
+ if (pos + 12 > len) break;
+ //fseek(f, pos, SEEK_SET);
+ f.Seek(pos, SeekOrigin.Begin);
+ //fread(buf + 2, 1, 12, f);
+ f.Read(buf, 0, 12);
+ }
+ }
+ }
+
+ // JPEG: (first two bytes of buf are first two bytes of the jpeg file; rest of buf is the DCT frame
+ if (buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF)
+ {
+ var height = (buf[7] << 8) + buf[8];
+ var width = (buf[9] << 8) + buf[10];
+
+ return new Size(width, height);
+ }
+
+ throw new UnknownImageFormatException();
+ }
+
+ private static Size DecodeJpeg2(BinaryReader reader)
+ {
+ bool found = false;
+ bool eof = false;
+
+ while (!found || eof)
+ {
+ // read 0xFF and the type
+ //reader.ReadByte();
+ byte type = reader.ReadByte();
+
+ // get length
+ int len = 0;
+ switch (type)
+ {
+ // start and end of the image
+ case 0xD8:
+ case 0xD9:
+ len = 0;
+ break;
+
+ // restart interval
+ case 0xDD:
+ len = 2;
+ break;
+
+ // the next two bytes is the length
+ default:
+ int lenHi = reader.ReadByte();
+ int lenLo = reader.ReadByte();
+ len = (lenHi << 8 | lenLo) - 2;
+ break;
+ }
+
+ // EOF?
+ if (type == 0xD9)
+ eof = true;
+
+ // process the data
+ if (len > 0)
+ {
+ // read the data
+ byte[] data = reader.ReadBytes(len);
+
+ // this is what we are looking for
+ if (type == 0xC0)
+ {
+ int width = data[1] << 8 | data[2];
+ int height = data[3] << 8 | data[4];
+ return new Size(width, height);
+ }
+ }
+ }
+
+ throw new UnknownImageFormatException();
+ }
+
+ private static Size DecodeJfif(BinaryReader reader)
+ {
+ while (reader.ReadByte() == 0xff)
+ {
+ byte marker = reader.ReadByte();
+ short chunkLength = ReadLittleEndianInt16(reader);
+ if (marker == 0xc0)
+ {
+ reader.ReadByte();
+ int height = ReadLittleEndianInt16(reader);
+ int width = ReadLittleEndianInt16(reader);
+ return new Size(width, height);
+ }
+
+ if (chunkLength < 0)
+ {
+ ushort uchunkLength = (ushort)chunkLength;
+ reader.ReadBytes(uchunkLength - 2);
+ }
+ else
+ {
+ reader.ReadBytes(chunkLength - 2);
+ }
+ }
+
+ throw new UnknownImageFormatException();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs b/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs
index d1ebc00c61..3187971ec6 100644
--- a/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs
+++ b/src/Libraries/SmartStore.Core/IO/LocalFileSystem.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@@ -13,7 +14,8 @@ public class LocalFileSystem : IFileSystem
{
private string _root;
private string _publicPath; // /Shop/base
- private string _storagePath; // C:\SMNET\base
+ private string _storagePath; // C:\SMNET\base
+ private bool _isCloudStorage; // When public URL is outside of current app
public LocalFileSystem()
: this(string.Empty, string.Empty)
@@ -38,6 +40,11 @@ protected internal LocalFileSystem(string basePath, string publicPath)
_root = basePath;
}
+ public bool IsCloudStorage
+ {
+ get { return _isCloudStorage; }
+ }
+
private void NormalizeStoragePath(ref string basePath, bool basePathIsAbsolute)
{
if (basePathIsAbsolute)
@@ -66,7 +73,12 @@ private string NormalizePublicPath(string publicPath, string basePath, bool base
{
if (publicPath.IsEmpty() || (!publicPath.StartsWith("~/") && !publicPath.IsWebUrl(true)))
{
- throw new ArgumentException("When the base path is a fully qualified path, the public path must not be empty, and either be a fully qualified URL or a virtual path (e.g.: ~/Media)", nameof(publicPath));
+ var streamMedia = CommonHelper.GetAppSetting("sm:StreamRemoteMedia", true);
+ if (!streamMedia)
+ {
+ throw new ArgumentException(@"When the base path is a fully qualified path and remote media streaming is disabled,
+ the public path must not be empty, and either be a fully qualified URL or a virtual path (e.g.: ~/Media)", nameof(publicPath));
+ }
}
}
@@ -80,12 +92,14 @@ private string NormalizePublicPath(string publicPath, string basePath, bool base
return appVirtualPath + publicPath.Substring(1);
}
- if (publicPath.IsEmpty())
+ if (publicPath.IsEmpty() && !basePathIsAbsolute)
{
// > /MyAppRoot/Media
return appVirtualPath + basePath;
}
+ _isCloudStorage = true;
+
return publicPath;
}
@@ -124,7 +138,7 @@ static string Fix(string path)
: path.TrimStart('/', '\\');
}
- public string GetPublicUrl(string path)
+ public string GetPublicUrl(string path, bool forCloud = false)
{
return MapPublic(path);
}
@@ -188,12 +202,26 @@ public IFolder GetFolderForFile(string path)
return new LocalFolder(Fix(folderPath), fileInfo.Directory);
}
- public IEnumerable SearchFiles(string path, string pattern)
+ public long CountFiles(string path, string pattern, Func predicate, bool deep = true)
{
- // get relative from absolute path
+ var files = SearchFiles(path, pattern, deep).AsParallel();
+
+ if (predicate != null)
+ {
+ return files.Count(predicate);
+ }
+ else
+ {
+ return files.Count();
+ }
+ }
+
+ public IEnumerable SearchFiles(string path, string pattern, bool deep = true)
+ {
+ // Get relative from absolute path
var index = _storagePath.EmptyNull().Length;
- return Directory.EnumerateFiles(MapStorage(path), pattern, SearchOption.AllDirectories)
+ return Directory.EnumerateFiles(MapStorage(path), pattern, deep ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
.Select(x => x.Substring(index));
}
@@ -207,10 +235,9 @@ public IEnumerable ListFiles(string path)
}
return directoryInfo
- .GetFiles()
+ .EnumerateFiles()
.Where(fi => !IsHidden(fi))
- .Select(fi => new LocalFile(Path.Combine(Fix(path), fi.Name), fi))
- .ToList();
+ .Select(fi => new LocalFile(Path.Combine(Fix(path), fi.Name), fi));
}
public IEnumerable ListFolders(string path)
@@ -234,10 +261,9 @@ public IEnumerable ListFolders(string path)
}
return directoryInfo
- .GetDirectories()
+ .EnumerateDirectories()
.Where(di => !IsHidden(di))
- .Select(di => new LocalFolder(Path.Combine(Fix(path), di.Name), di))
- .ToList();
+ .Select(di => new LocalFolder(Path.Combine(Fix(path), di.Name), di));
}
private static bool IsHidden(FileSystemInfo di)
@@ -456,6 +482,7 @@ private class LocalFile : IFile
{
private readonly string _path;
private readonly FileInfo _fileInfo;
+ private Size? _dimensions;
public LocalFile(string path, FileInfo fileInfo)
{
@@ -468,11 +495,21 @@ public string Path
get { return _path; }
}
+ public string Directory
+ {
+ get { return _path.Substring(0, _path.Length - Name.Length); }
+ }
+
public string Name
{
get { return _fileInfo.Name; }
}
+ public string Title
+ {
+ get { return System.IO.Path.GetFileNameWithoutExtension(_fileInfo.Name); }
+ }
+
public long Size
{
get { return _fileInfo.Length; }
@@ -483,11 +520,32 @@ public DateTime LastUpdated
get { return _fileInfo.LastWriteTime; }
}
- public string FileType
+ public string Extension
{
get { return _fileInfo.Extension; }
}
+ public Size Dimensions
+ {
+ get
+ {
+ if (_dimensions == null)
+ {
+ try
+ {
+ var mime = MimeTypes.MapNameToMimeType(_fileInfo.Name);
+ _dimensions = ImageHeader.GetDimensions(OpenRead(), mime, false);
+ }
+ catch
+ {
+ _dimensions = new Size();
+ }
+ }
+
+ return _dimensions.Value;
+ }
+ }
+
public bool Exists
{
get { return _fileInfo.Exists; }
diff --git a/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs b/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs
index 337d1032ae..ce8c87c8b1 100644
--- a/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs
+++ b/src/Libraries/SmartStore.Core/Linq/Expanders/LambdaPathExpander.cs
@@ -2,14 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
-using System.Linq.Dynamic;
-using System.ComponentModel;
using System.Reflection;
using SmartStore.Utilities;
namespace SmartStore.Linq
{
-
public class LambdaPathExpander : IPathExpander
{
private readonly IList _expands;
diff --git a/src/Libraries/SmartStore.Core/Linq/PredicateBuilder.cs b/src/Libraries/SmartStore.Core/Linq/PredicateBuilder.cs
index 7a6bdf9b88..06101c7c6d 100644
--- a/src/Libraries/SmartStore.Core/Linq/PredicateBuilder.cs
+++ b/src/Libraries/SmartStore.Core/Linq/PredicateBuilder.cs
@@ -4,7 +4,6 @@
namespace SmartStore.Linq
{
-
public static class PredicateBuilder
{
public static Expression> True()
@@ -32,7 +31,5 @@ public static Expression> And(this Expression> ex
return Expression.Lambda>
(Expression.And(expr1.Body, invokedExpr), expr1.Parameters);
}
-
}
-
}
\ No newline at end of file
diff --git a/src/Libraries/SmartStore.Core/Localization/ILocalizationFileResolver.cs b/src/Libraries/SmartStore.Core/Localization/ILocalizationFileResolver.cs
new file mode 100644
index 0000000000..e734c92653
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Localization/ILocalizationFileResolver.cs
@@ -0,0 +1,44 @@
+using System;
+
+namespace SmartStore.Core.Localization
+{
+ ///
+ /// Responsible for finding a localization file for client scripts
+ ///
+ public interface ILocalizationFileResolver
+ {
+ ///
+ /// Tries to find a matching localization file for a given culture in the following order
+ /// (assuming is 'de-DE', is 'lang-*.js' and is 'en-US'):
+ ///
+ /// - Exact match > lang-de-DE.js
+ /// - Neutral culture > lang-de.js
+ /// - Any region for language > lang-de-CH.js
+ /// - Exact match for fallback culture > lang-en-US.js
+ /// - Neutral fallback culture > lang-en.js
+ /// - Any region for fallback language > lang-en-GB.js
+ ///
+ ///
+ /// The ISO culture code to get a localization file for, e.g. 'de-DE'
+ /// The virtual path to search in
+ /// The pattern to match, e.g. 'lang-*.js'. The wildcard char MUST exist.
+ ///
+ /// Whether caching should be enabled. If false, no attempt is made to read from cache, nor writing the result to the cache.
+ /// Cache duration is 24 hours. Automatic eviction on file change is NOT performed.
+ ///
+ /// Optional.
+ /// Result
+ LocalizationFileResolveResult Resolve(
+ string culture,
+ string virtualPath,
+ string pattern,
+ bool cache = true,
+ string fallbackCulture = "en");
+ }
+
+ public class LocalizationFileResolveResult
+ {
+ public string Culture { get; set; }
+ public string VirtualPath { get; set; }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Localization/LocalizationFileResolver.cs b/src/Libraries/SmartStore.Core/Localization/LocalizationFileResolver.cs
new file mode 100644
index 0000000000..fc02435c1e
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Localization/LocalizationFileResolver.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Web;
+using SmartStore.Core.Caching;
+using SmartStore.Utilities;
+
+namespace SmartStore.Core.Localization
+{
+ public class LocalizationFileResolver : ILocalizationFileResolver
+ {
+ private readonly ICacheManager _cache;
+
+ public LocalizationFileResolver(ICacheManager cache)
+ {
+ _cache = cache;
+ }
+
+ public LocalizationFileResolveResult Resolve(
+ string culture,
+ string virtualPath,
+ string pattern,
+ bool cache = true,
+ string fallbackCulture = "en")
+ {
+ Guard.NotEmpty(culture, nameof(culture));
+ Guard.NotEmpty(virtualPath, nameof(virtualPath));
+ Guard.NotEmpty(pattern, nameof(pattern));
+
+ if (pattern.IndexOf('*') < 0)
+ {
+ throw new ArgumentException("The pattern must contain a wildcard char for substitution, e.g. 'lang-*.js'.", nameof(pattern));
+ }
+
+ virtualPath = FixPath(virtualPath);
+ var cacheKey = "core:locfile:" + virtualPath.ToLower() + pattern + "/" + culture;
+ string result = null;
+
+ if (cache && _cache.Contains(cacheKey))
+ {
+ result = _cache.Get(cacheKey);
+ return result != null ? CreateResult(result, virtualPath, pattern) : null;
+ }
+
+ if (!LocalizationHelper.IsValidCultureCode(culture))
+ {
+ throw new ArgumentException($"'{culture}' is not a valid culture code.", nameof(culture));
+ }
+
+ var ci = CultureInfo.GetCultureInfo(culture);
+ var directory = new DirectoryInfo(CommonHelper.MapPath(virtualPath, false));
+
+ if (!directory.Exists)
+ {
+ throw new DirectoryNotFoundException($"Path '{virtualPath}' does not exist.");
+ }
+
+ // 1: Match passed culture
+ result = ResolveMatchingFile(ci, directory, pattern);
+
+ if (result == null && fallbackCulture.HasValue() && culture != fallbackCulture)
+ {
+ if (!LocalizationHelper.IsValidCultureCode(fallbackCulture))
+ {
+ throw new ArgumentException($"'{culture}' is not a valid culture code.", nameof(fallbackCulture));
+ }
+
+ // 2: Match fallback culture
+ ci = CultureInfo.GetCultureInfo(fallbackCulture);
+ result = ResolveMatchingFile(ci, directory, pattern);
+ }
+
+ if (cache)
+ {
+ _cache.Put(cacheKey, result, TimeSpan.FromHours(24));
+ }
+
+ if (result.HasValue())
+ {
+ return CreateResult(result, virtualPath, pattern);
+ }
+
+ return null;
+ }
+
+ private string ResolveMatchingFile(CultureInfo ci, DirectoryInfo directory, string pattern)
+ {
+ string result = null;
+
+ // 1: Exact match
+ // -----------------------------------------------------
+ var fileName = pattern.Replace("*", ci.Name);
+ if (File.Exists(Path.Combine(directory.FullName, fileName)))
+ {
+ result = ci.Name;
+ }
+
+ // 2: Match neutral culture, e.g. de-DE > de
+ // -----------------------------------------------------
+ if (result == null && !ci.IsNeutralCulture && ci.Parent != null)
+ {
+ ci = ci.Parent;
+ fileName = pattern.Replace("*", ci.Name);
+ if (File.Exists(Path.Combine(directory.FullName, fileName)))
+ {
+ result = ci.Name;
+ }
+ }
+
+ // 2: Match any region, e.g. de-DE > de-CH
+ // -----------------------------------------------------
+ if (result == null && ci.IsNeutralCulture)
+ {
+ // Convert pattern to Regex: "lang-*.js" > "^lang.(.+?).js$"
+ var rgPattern = "^" + pattern.Replace("*", @"(.+?)") + "$";
+ var rgFileName = new Regex(rgPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ foreach (var fi in directory.EnumerateFiles(pattern.Replace("*", ci.Name + "-*"), SearchOption.TopDirectoryOnly))
+ {
+ var culture = rgFileName.Match(fi.Name).Groups[1].Value;
+ if (LocalizationHelper.IsValidCultureCode(culture))
+ {
+ result = culture;
+ break;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private string FixPath(string virtualPath)
+ {
+ return VirtualPathUtility.ToAppRelative(virtualPath).EnsureEndsWith("/");
+ }
+
+ private LocalizationFileResolveResult CreateResult(string culture, string virtualPath, string pattern)
+ {
+ var fileName = pattern.Replace("*", culture);
+ return new LocalizationFileResolveResult
+ {
+ Culture = culture,
+ VirtualPath = VirtualPathUtility.ToAbsolute(virtualPath + fileName)
+ };
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Localization/LocalizedString.cs b/src/Libraries/SmartStore.Core/Localization/LocalizedString.cs
index 6450a522cd..a09c159735 100644
--- a/src/Libraries/SmartStore.Core/Localization/LocalizedString.cs
+++ b/src/Libraries/SmartStore.Core/Localization/LocalizedString.cs
@@ -44,7 +44,18 @@ public string Text
get { return _localized; }
}
- public static implicit operator string(LocalizedString obj)
+ ///
+ /// Returns a js encoded string which already contains delimiters.
+ ///
+ public IHtmlString JsText
+ {
+ get
+ {
+ return System.Web.Mvc.MvcHtmlString.Create(_localized.EncodeJsString());
+ }
+ }
+
+ public static implicit operator string(LocalizedString obj)
{
return obj.Text;
}
diff --git a/src/Libraries/SmartStore.Core/Localization/Localizer.cs b/src/Libraries/SmartStore.Core/Localization/Localizer.cs
index 82e62d9d24..bb96ea085b 100644
--- a/src/Libraries/SmartStore.Core/Localization/Localizer.cs
+++ b/src/Libraries/SmartStore.Core/Localization/Localizer.cs
@@ -1,4 +1,5 @@
namespace SmartStore.Core.Localization
{
public delegate LocalizedString Localizer(string key, params object[] args);
+ public delegate LocalizedString LocalizerEx(string key, int languageId, params object[] args);
}
\ No newline at end of file
diff --git a/src/Libraries/SmartStore.Core/Localization/NullLocalizer.cs b/src/Libraries/SmartStore.Core/Localization/NullLocalizer.cs
index f325439740..0b43a01c1d 100644
--- a/src/Libraries/SmartStore.Core/Localization/NullLocalizer.cs
+++ b/src/Libraries/SmartStore.Core/Localization/NullLocalizer.cs
@@ -1,22 +1,26 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
namespace SmartStore.Core.Localization
{
public static class NullLocalizer
{
- private static readonly Localizer s_instance;
-
+ private static readonly Localizer _instance;
+ private static readonly LocalizerEx _instanceEx;
+
static NullLocalizer()
{
- s_instance = (format, args) => new LocalizedString((args == null || args.Length == 0) ? format : string.Format(format, args));
+ _instance = (format, args) => new LocalizedString((args == null || args.Length == 0) ? format : string.Format(format, args));
+ _instanceEx = (format, languageId, args) => new LocalizedString((args == null || args.Length == 0) ? format : string.Format(format, args));
}
public static Localizer Instance
{
- get { return s_instance; }
+ get { return _instance; }
+ }
+
+ public static LocalizerEx InstanceEx
+ {
+ get { return _instanceEx; }
}
}
}
diff --git a/src/Libraries/SmartStore.Core/Logging/Notifier.cs b/src/Libraries/SmartStore.Core/Logging/Notifier.cs
index be0e903d61..ed2a00ba94 100644
--- a/src/Libraries/SmartStore.Core/Logging/Notifier.cs
+++ b/src/Libraries/SmartStore.Core/Logging/Notifier.cs
@@ -6,7 +6,6 @@
namespace SmartStore.Core.Logging
{
-
public interface INotifier
{
void Add(NotifyType type, LocalizedString message, bool durable = true);
diff --git a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs
index bdf0c42bbd..10da774027 100644
--- a/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs
+++ b/src/Libraries/SmartStore.Core/Logging/TraceLogger.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Text;
using SmartStore.Core.Domain.Customers;
using SmartStore.Core.Domain.Logging;
@@ -75,7 +76,10 @@ public void Log(LogLevel level, Exception exception, string message, object[] ar
if (message.HasValue())
{
- var msg = (args == null ? message : message.FormatInvariant(args));
+ var msg = args != null && args.Any()
+ ? message.FormatInvariant(args)
+ : message;
+
_traceSource.TraceEvent(type, (int)type, "{0}: {1}".FormatCurrent(type.ToString().ToUpper(), msg));
}
}
diff --git a/src/Libraries/SmartStore.Core/PagedList.cs b/src/Libraries/SmartStore.Core/PagedList.cs
index 565ff19c3b..87af23cda8 100644
--- a/src/Libraries/SmartStore.Core/PagedList.cs
+++ b/src/Libraries/SmartStore.Core/PagedList.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections;
+using System.Collections.Generic;
using System.Linq;
namespace SmartStore.Core
@@ -148,6 +149,16 @@ public PagedList(int pageIndex, int pageSize, int totalItemsCount)
: base(pageIndex, pageSize, totalItemsCount)
{
}
- }
+
+ public static PagedList Create(IEnumerable source, int pageIndex, int pageSize)
+ {
+ return new PagedList(source, pageIndex, pageSize);
+ }
+
+ public static PagedList Create(IEnumerable source, int pageIndex, int pageSize, int totalCount)
+ {
+ return new PagedList(source, pageIndex, pageSize, totalCount);
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/PagedList`T.cs b/src/Libraries/SmartStore.Core/PagedList`T.cs
index 2001a777f1..1614557a1e 100644
--- a/src/Libraries/SmartStore.Core/PagedList`T.cs
+++ b/src/Libraries/SmartStore.Core/PagedList`T.cs
@@ -19,6 +19,7 @@ public class PagedList : IPagedList, IReadOnlyList, IReadOnlyCollection
private List _list;
+ /// The 0-based page index
public PagedList(IEnumerable source, int pageIndex, int pageSize)
{
Guard.NotNull(source, "source");
@@ -26,6 +27,7 @@ public PagedList(IEnumerable source, int pageIndex, int pageSize)
Init(source.AsQueryable(), pageIndex, pageSize, null);
}
+ /// The 0-based page index
public PagedList(IEnumerable source, int pageIndex, int pageSize, int totalCount)
{
Guard.NotNull(source, "source");
diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs b/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs
index 240a788883..35b2b3e8c9 100644
--- a/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs
+++ b/src/Libraries/SmartStore.Core/Plugins/PluginDescriptor.cs
@@ -42,11 +42,16 @@ public PluginDescriptor(Assembly referencedAssembly, FileInfo originalAssemblyFi
///
public string PhysicalPath { get; set; }
- ///
- /// Gets the file name of the brand image (without path)
- /// or an empty string if no image is specified
- ///
- public string BrandImageFileName
+ ///
+ /// The virtual path of the runtime plugin
+ ///
+ public string VirtualPath { get; set; }
+
+ ///
+ /// Gets the file name of the brand image (without path)
+ /// or an empty string if no image is specified
+ ///
+ public string BrandImageFileName
{
get
{
diff --git a/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs b/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs
index 072b362276..6a95a30aae 100644
--- a/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs
+++ b/src/Libraries/SmartStore.Core/Plugins/PluginManager.cs
@@ -15,6 +15,7 @@
using SmartStore.Core.Packaging;
using SmartStore.Utilities;
using SmartStore.Utilities.Threading;
+using SmartStore.Core.Data;
// Contributor: Umbraco (http://www.umbraco.com). Thanks a lot!
// SEE THIS POST for full details of what this does
@@ -185,7 +186,7 @@ public static void Initialize()
}
}
- if (dirty)
+ if (dirty && DataSettings.DatabaseIsInstalled())
{
// Save current hash of all deployed plugins to disk
var hash = ComputePluginsHash(_referencedPlugins.Values.OrderBy(x => x.FolderName).ToArray());
@@ -259,6 +260,8 @@ private static PluginDescriptor LoadPluginDescriptor(DirectoryInfo d, ICollectio
throw new SmartException("The plugin descriptor '{0}' does not define a plugin assembly file name. Try assigning the plugin a file name and recompile.".FormatInvariant(descriptionFile.FullName));
}
+ descriptor.VirtualPath = _pluginsPath + "/" + descriptor.FolderName;
+
// Set 'Installed' property
descriptor.Installed = installedPluginSystemNames.Contains(descriptor.SystemName);
diff --git a/src/Libraries/SmartStore.Core/RouteInfo.cs b/src/Libraries/SmartStore.Core/RouteInfo.cs
index d58573ebd5..84827ddd44 100644
--- a/src/Libraries/SmartStore.Core/RouteInfo.cs
+++ b/src/Libraries/SmartStore.Core/RouteInfo.cs
@@ -2,9 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Web.Routing;
+using Newtonsoft.Json;
namespace SmartStore
{
+ [JsonConverter(typeof(RouteInfoConverter))]
public class RouteInfo
{
public RouteInfo(RouteInfo cloneFrom)
@@ -39,6 +41,7 @@ public RouteInfo(string action, RouteValueDictionary routeValues)
{
}
+ [JsonConstructor]
public RouteInfo(string action, string controller, RouteValueDictionary routeValues)
{
Guard.NotEmpty(action, nameof(action));
@@ -66,6 +69,65 @@ public RouteValueDictionary RouteValues
get;
private set;
}
+ }
+
+ #region JsonConverter
+
+ public class RouteInfoConverter : JsonConverter
+ {
+ public override bool CanWrite
+ {
+ get { return false; }
+ }
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(RouteInfo);
+ }
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ string action = null;
+ string controller = null;
+ RouteValueDictionary routeValues = null;
+
+ reader.Read();
+ while (reader.TokenType == JsonToken.PropertyName)
+ {
+ string a = reader.Value.ToString();
+ if (string.Equals(a, "Action", StringComparison.OrdinalIgnoreCase))
+ {
+ reader.Read();
+ action = serializer.Deserialize(reader);
+ }
+ else if (string.Equals(a, "Controller", StringComparison.OrdinalIgnoreCase))
+ {
+ reader.Read();
+ controller = serializer.Deserialize(reader);
+ }
+ else if (string.Equals(a, "RouteValues", StringComparison.OrdinalIgnoreCase))
+ {
+ reader.Read();
+ routeValues = serializer.Deserialize(reader);
+ }
+ else
+ {
+ reader.Skip();
+ }
+
+ reader.Read();
+ }
+
+ var routeInfo = Activator.CreateInstance(objectType, new object[] { action, controller, routeValues });
+
+ return routeInfo;
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new NotSupportedException();
+ }
}
+
+ #endregion
}
diff --git a/src/Libraries/SmartStore.Core/Search/SearchQuery.cs b/src/Libraries/SmartStore.Core/Search/SearchQuery.cs
index cdfabda9bf..6cd4f78b9e 100644
--- a/src/Libraries/SmartStore.Core/Search/SearchQuery.cs
+++ b/src/Libraries/SmartStore.Core/Search/SearchQuery.cs
@@ -25,12 +25,12 @@ public SearchQuery()
{
}
- public SearchQuery(string field, string term, SearchMode mode = SearchMode.StartsWith, bool escape = false, bool isFuzzySearch = false)
+ public SearchQuery(string field, string term, SearchMode mode = SearchMode.Contains, bool escape = false, bool isFuzzySearch = false)
: base(field.HasValue() ? new[] { field } : null, term, mode, escape, isFuzzySearch)
{
}
- public SearchQuery(string[] fields, string term, SearchMode mode = SearchMode.StartsWith, bool escape = false, bool isFuzzySearch = false)
+ public SearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = false, bool isFuzzySearch = false)
: base(fields, term, mode, escape, isFuzzySearch)
{
}
@@ -41,7 +41,7 @@ public class SearchQuery : ISearchQuery where TQuery : class, ISearchQue
private readonly Dictionary _facetDescriptors;
private Dictionary _customData;
- protected SearchQuery(string[] fields, string term, SearchMode mode = SearchMode.StartsWith, bool escape = false, bool isFuzzySearch = false)
+ protected SearchQuery(string[] fields, string term, SearchMode mode = SearchMode.Contains, bool escape = false, bool isFuzzySearch = false)
{
Fields = fields;
Term = term;
diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj
index cca810df42..c9344cd7ff 100644
--- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj
+++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj
@@ -10,7 +10,7 @@
Properties
SmartStore.Core
SmartStore.Core
- v4.5.2
+ v4.6.1
512
@@ -62,6 +62,9 @@
MinimumRecommendedRules.ruleset
+
+ ..\..\packages\AngleSharp.0.9.9\lib\net45\AngleSharp.dll
+
..\..\packages\Autofac.4.5.0\lib\net45\Autofac.dll
True
@@ -71,15 +74,11 @@
True
- False
- ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll
+ ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll
+ True
- False
- ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll
-
-
- ..\..\packages\HtmlAgilityPack.1.4.9.5\lib\Net45\HtmlAgilityPack.dll
+ ..\..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll
True
@@ -120,6 +119,9 @@
+
+ ..\..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll
+
False
@@ -179,7 +181,10 @@
+
+
+
@@ -203,6 +208,7 @@
+
@@ -217,6 +223,7 @@
+
@@ -224,8 +231,10 @@
+
+
@@ -246,6 +255,7 @@
+
@@ -258,6 +268,8 @@
+
+
@@ -396,9 +408,6 @@
-
-
-
@@ -454,12 +463,18 @@
+
+
+
+
+
+
-
+
@@ -505,7 +520,6 @@
-
@@ -664,6 +678,7 @@
+
diff --git a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs
index cb55e7d3ed..47c338ac27 100644
--- a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs
+++ b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs
@@ -24,7 +24,7 @@ public static class SmartStoreVersion
new Version("3.0")
};
- private const string HELP_BASEURL = "http://docs.smartstore.com/display/";
+ private const string HELP_BASEURL = "https://docs.smartstore.com/display/";
static SmartStoreVersion()
{
diff --git a/src/Libraries/SmartStore.Core/Templating/DefaultTemplateManager.cs b/src/Libraries/SmartStore.Core/Templating/DefaultTemplateManager.cs
new file mode 100644
index 0000000000..8395d04364
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Templating/DefaultTemplateManager.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+
+namespace SmartStore.Templating
+{
+ public partial class DefaultTemplateManager : ITemplateManager
+ {
+ private readonly ConcurrentDictionary _templates;
+ private readonly ITemplateEngine _engine;
+
+ public DefaultTemplateManager(ITemplateEngine engine)
+ {
+ _templates = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+ _engine = engine;
+ }
+
+ public IReadOnlyDictionary All()
+ {
+ return _templates;
+ }
+
+ public bool Contains(string name)
+ {
+ Guard.NotEmpty(name, nameof(name));
+
+ return _templates.ContainsKey(name);
+ }
+
+ public ITemplate Get(string name)
+ {
+ Guard.NotEmpty(name, nameof(name));
+
+ _templates.TryGetValue(name, out var template);
+ return template;
+ }
+
+ public void Put(string name, ITemplate template)
+ {
+ Guard.NotEmpty(name, nameof(name));
+ Guard.NotNull(template, nameof(template));
+
+ _templates[name] = template;
+ }
+
+ public ITemplate GetOrAdd(string name, Func sourceFactory)
+ {
+ Guard.NotEmpty(name, nameof(name));
+ Guard.NotNull(sourceFactory, nameof(sourceFactory));
+
+ return _templates.GetOrAdd(name, key =>
+ {
+ return _engine.Compile(sourceFactory());
+ });
+ }
+
+ public bool TryRemove(string name, out ITemplate template)
+ {
+ Guard.NotEmpty(name, nameof(name));
+
+ return _templates.TryRemove(name, out template);
+ }
+
+ public void Clear()
+ {
+ _templates.Clear();
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Templating/ITemplate.cs b/src/Libraries/SmartStore.Core/Templating/ITemplate.cs
new file mode 100644
index 0000000000..3014ba0e20
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Templating/ITemplate.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace SmartStore.Templating
+{
+ ///
+ /// A compiled template intended to be stored in a singleton storage.
+ ///
+ public interface ITemplate
+ {
+ ///
+ /// Gets the original template source
+ ///
+ string Source { get; }
+
+ ///
+ /// Renders the template in
+ ///
+ ///
+ /// The model object which contains the data for the template.
+ /// Can be a subclass of ,
+ /// a plain class object, or an anonymous type.
+ ///
+ /// Provider to use for formatting numbers, dates, money etc.
+ /// The processed template result
+ string Render(object model, IFormatProvider formatProvider);
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Templating/ITemplateEngine.cs b/src/Libraries/SmartStore.Core/Templating/ITemplateEngine.cs
new file mode 100644
index 0000000000..56e5868375
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Templating/ITemplateEngine.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using SmartStore.Core;
+
+namespace SmartStore.Templating
+{
+ ///
+ /// Represents a model object for testing purposes. A test model
+ /// becomes necessary when the database does not contain any data
+ /// for the previews/tested model.
+ ///
+ public interface ITestModel
+ {
+ ///
+ /// The name of the model to use as hash key, e.g. Product, Order, Customer etc.
+ ///
+ string ModelName { get; }
+ }
+
+ ///
+ /// Responsible for compiling and rendering the templates
+ ///
+ public interface ITemplateEngine
+ {
+ ///
+ /// Compiles a template for faster rendering
+ ///
+ /// The template source
+ /// Compiled template
+ ITemplate Compile(string source);
+
+ ///
+ /// Directly renders a template source
+ ///
+ /// The template source
+ ///
+ /// The model object which contains the data for the template.
+ /// Can be a subclass of ,
+ /// a plain class object, or an anonymous type.
+ ///
+ /// Provider to use for formatting numbers, dates, money etc.
+ /// The processed template result
+ string Render(string source, object model, IFormatProvider formatProvider);
+
+ ///
+ /// Creates a test model for the passed entity to be used during preview and test.
+ ///
+ /// The entity to create a test model for.
+ /// The model prefix
+ /// An object which implements and contains some test data for the declared properties of
+ ITestModel CreateTestModelFor(BaseEntity entity, string modelPrefix);
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Templating/ITemplateManager.cs b/src/Libraries/SmartStore.Core/Templating/ITemplateManager.cs
new file mode 100644
index 0000000000..affe05aeb1
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Templating/ITemplateManager.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+
+namespace SmartStore.Templating
+{
+ ///
+ /// Responsible for managing all compiled templates
+ ///
+ public interface ITemplateManager
+ {
+ ///
+ /// Gets all compiled templates
+ ///
+ ///
+ IReadOnlyDictionary All();
+
+ ///
+ /// Checks whether a compiled template exists in the storage
+ ///
+ /// Template name
+ /// true when the template exists
+ bool Contains(string name);
+
+ ///
+ /// Gets a compiled template matching the passed name
+ ///
+ /// Template name to get.
+ /// An instance of or null
+ ITemplate Get(string name);
+
+ ///
+ /// Saves a compiled template in the storage. Any existing template gets overridden.
+ ///
+ /// The name to use as template key
+ /// The compiled template
+ void Put(string name, ITemplate template);
+
+ ///
+ /// Either gets a template with the passed name, or compiles and stores
+ /// a template if it does not exist yet.
+ ///
+ /// Template name
+ /// The factory used to generate/obtain the template source.
+ /// The compiled template
+ ITemplate GetOrAdd(string name, Func sourceFactory);
+
+ ///
+ /// Attempts to remove and return the compiled template that has the specified name.
+ ///
+ /// The name of the template to remove and return.
+ /// The removed template or null
+ /// true if the template was removed, false otherwise.
+ bool TryRemove(string name, out ITemplate template);
+
+ ///
+ /// Removes all compiled templates from the storage.
+ ///
+ void Clear();
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Templating/NullTemplateEngine.cs b/src/Libraries/SmartStore.Core/Templating/NullTemplateEngine.cs
new file mode 100644
index 0000000000..04f166dbf9
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Templating/NullTemplateEngine.cs
@@ -0,0 +1,49 @@
+using System;
+using SmartStore.Core;
+
+namespace SmartStore.Templating
+{
+ public class NullTemplateEngine : ITemplateEngine
+ {
+ private readonly static ITemplateEngine _instance = new NullTemplateEngine();
+
+ public static ITemplateEngine Instance => _instance;
+
+ public ITemplate Compile(string template)
+ {
+ return new NullTemplate(template);
+ }
+
+ public string Render(string source, object data, IFormatProvider formatProvider)
+ {
+ return source;
+ }
+
+ public ITestModel CreateTestModelFor(BaseEntity entity, string modelPrefix)
+ {
+ return new NullTestModel();
+ }
+
+ internal class NullTestModel : ITestModel
+ {
+ public string ModelName => "TestModel";
+ }
+
+ internal class NullTemplate : ITemplate
+ {
+ private readonly string _source;
+
+ public NullTemplate(string source)
+ {
+ _source = source;
+ }
+
+ public string Source => _source;
+
+ public string Render(object data, IFormatProvider formatProvider)
+ {
+ return _source;
+ }
+ }
+ }
+}
diff --git a/src/Presentation/SmartStore.Web.Framework/Theming/IThemeContext.cs b/src/Libraries/SmartStore.Core/Themes/IThemeContext.cs
similarity index 94%
rename from src/Presentation/SmartStore.Web.Framework/Theming/IThemeContext.cs
rename to src/Libraries/SmartStore.Core/Themes/IThemeContext.cs
index a019d3761b..1162ab331d 100644
--- a/src/Presentation/SmartStore.Web.Framework/Theming/IThemeContext.cs
+++ b/src/Libraries/SmartStore.Core/Themes/IThemeContext.cs
@@ -1,6 +1,5 @@
-using SmartStore.Core.Themes;
-
-namespace SmartStore.Web.Framework.Theming
+
+namespace SmartStore.Core.Themes
{
///
/// Work context
diff --git a/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs b/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs
index 87052c3e25..09c40503e8 100644
--- a/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs
+++ b/src/Libraries/SmartStore.Core/Themes/InheritedThemeFileResult.cs
@@ -35,5 +35,10 @@ public class InheritedThemeFileResult
public string ResultThemeName { get; set; }
public bool IsExplicit { get; set; }
+
+ ///
+ /// The query string, e.g. '?explicit'
+ ///
+ public string Query { get; set; }
}
}
diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs b/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs
index 2528df7b9e..739c8a40b3 100644
--- a/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs
+++ b/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs
@@ -9,10 +9,8 @@
namespace SmartStore.Core.Themes
{
-
public class ThemeManifest : ComparableObject
{
-
internal ThemeManifest()
{
}
@@ -321,5 +319,4 @@ public enum ThemeManifestState
MissingBaseTheme = -1,
Active = 0,
}
-
}
diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs b/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs
index 2cf7283992..6fadd1910e 100644
--- a/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs
+++ b/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs
@@ -7,8 +7,7 @@
using SmartStore.Collections;
namespace SmartStore.Core.Themes
-{
-
+{
internal class ThemeManifestMaterializer
{
private readonly ThemeManifest _manifest;
@@ -171,7 +170,5 @@ private ThemeVariableType ConvertVarType(string type, XmlElement affected, out s
return result;
}
-
}
-
}
diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeVariableInfo.cs b/src/Libraries/SmartStore.Core/Themes/ThemeVariableInfo.cs
index e6db1d7aec..0fbb373a9d 100644
--- a/src/Libraries/SmartStore.Core/Themes/ThemeVariableInfo.cs
+++ b/src/Libraries/SmartStore.Core/Themes/ThemeVariableInfo.cs
@@ -3,14 +3,12 @@
using System.Linq;
namespace SmartStore.Core.Themes
-{
-
+{
///
/// Represents deserialized metadata for a theme variable
///
public class ThemeVariableInfo : DisposableObject
{
-
///
/// Gets the variable name as specified in the config file
///
@@ -59,5 +57,4 @@ protected override void OnDispose(bool disposing)
}
}
}
-
}
diff --git a/src/Libraries/SmartStore.Core/Utilities/ColorHelper.cs b/src/Libraries/SmartStore.Core/Utilities/ColorHelper.cs
deleted file mode 100644
index 50dfc83261..0000000000
--- a/src/Libraries/SmartStore.Core/Utilities/ColorHelper.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using System;
-using System.Drawing;
-
-namespace SmartStore.Utilities
-{
- public static class ColorHelper
- {
- public static int GetPerceivedBrightness(string htmlColor)
- {
- if (String.IsNullOrEmpty(htmlColor))
- htmlColor = "#ffffff";
-
- return GetPerceivedBrightness(ColorTranslator.FromHtml(htmlColor));
- }
-
- ///
- /// Calculates the perceived brightness of a color.
- ///
- /// The color
- ///
- /// A number in the range of 0 (black) to 255 (White).
- /// For text contrast colors, an optimal cutoff value is 130.
- ///
- public static int GetPerceivedBrightness(Color color)
- {
- return (int)Math.Sqrt(
- color.R * color.R * .241 +
- color.G * color.G * .691 +
- color.B * color.B * .068);
- }
- }
-}
diff --git a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs
index 9db88283f4..a6d0649e24 100644
--- a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs
+++ b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs
@@ -1,7 +1,9 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Dynamic;
+using System.Linq;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
@@ -180,7 +182,7 @@ public static IDictionary ObjectToDictionary(object obj)
return FastProperty.ObjectToDictionary(
obj,
- key => key.Replace("_", "-"));
+ key => key.Replace("_", "-").Replace("@", ""));
}
///
@@ -230,5 +232,38 @@ private static bool TryAction(Func func, out T output)
return false;
}
}
+
+ public static bool IsTruthy(object value)
+ {
+ if (value == null)
+ return false;
+
+ switch (value)
+ {
+ case string x:
+ return x.HasValue();
+ case bool x:
+ return x == true;
+ case DateTime x:
+ return x > DateTime.MinValue;
+ case TimeSpan x:
+ return x > TimeSpan.MinValue;
+ case Guid x:
+ return x != Guid.Empty;
+ case IComparable x:
+ return x.CompareTo(0) != 0;
+ case IEnumerable