diff --git a/.editorconfig b/.editorconfig
index 892b63c33c..f731f7194a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,6 +1,6 @@
-root = true
+# root = true
# Unix-style newlines with a newline ending every file
-[*]
-indent_style = tab
-indent_size = 4
+# [*]
+# indent_style = tab
+# indent_size = 4
diff --git a/.gitignore b/.gitignore
index 336b64107a..118d11be55 100644
--- a/.gitignore
+++ b/.gitignore
@@ -320,3 +320,4 @@ Kopie von*
src/SmartStoreNET.Packager.sln
Log.txt
+src/Presentation/SmartStore.Web/Themes/FlexMuseo/
diff --git a/CREDITS.txt b/CREDITS.txt
index e421722989..5ea4c4a6fb 100644
--- a/CREDITS.txt
+++ b/CREDITS.txt
@@ -52,11 +52,11 @@ Copyright: Copyright Andrey Taritsyn 2014
License: Apache License 2.0 (Apache)
-CKEditor
+summernote
---------------------------------------------
-WebSite: http://ckeditor.com/
-Copyright: © 2014 CKSource - Frederico Knabben
-License: GNU Library General Public License (LGPL)
+WebSite: https://summernote.org/
+Copyright: Copyright (c) 2015~ Summernote Team (https://github.com/orgs/summernote/people)
+License: MIT
DotNetOpenAuth
diff --git a/README.md b/README.md
index bd0524f866..aa25d27b32 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC
* Unlimited number of products and categories
* Product Bundles
* RESTful WebApi
-* Multi-language support
+* Multi-language and RTL support
* Modern, clean, SEO-optimized and fully responsive Theme based on Bootstrap 4
* Ultra fast search framework with faceted search support
* Extremely scalable thanks to output caching, REDIS & Microsoft Azure support.
@@ -56,22 +56,23 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC
* and many more...
## Project Status
-SmartStore.NET V3.0.0 has been released on May 15, 2017. The highlights are:
-
-* **Flex**: New mobile-first responsive Theme based on Bootstrap 4
-* **Mega Search \***: highly professional search framework based on Lucene.NET
- * Ultra fast search results, even with millions of items
- * Faceted search
- * Synonyms
- * Compound word splitting
-* **Mega Menu \***: highly customizable catalog menu widgets
-* **Content Slider \***: creates eye-catching content to boost sales
-* **Output Cache \*** with "donut hole caching" for maximum speed and scalability
-* **Microsoft AZURE \*** provider for media storage
-* Web Farms: **REDIS \*** providers for Business Cache, Output Cache and Session State
-* Product variant **option sets**
-* New product specification attribute type: **numeric range**
-* Image support for variant attributes
+SmartStore.NET V3.1.0 has been released on April 20, 2018. The highlights are:
+
+* **Wallet \***: Enables full or partial order payment via credit account. Includes REST-Api.
+* **[Liquid](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers) template engine**: very flexible templating for e-mails and campaigns with autocompletion and syntax highlighting.
+* **Cash Rounding**: define money rounding rules on currency and payment method level.
+* **Modern, responsive backend**: migrated backend to Bootstrap 4, overhauled and improved the user interface.
+* **Enhanced MegaMenu \***: virtual dropdowns for surplus top-level categories and brands.
+* **RTL**: comprehensive RTL (Right-to-left) and bidi(rectional) support.
+* **Amazon Pay**:
+ * Supports merchants registered in the USA and Japan
+ * External authentication via *Login with Amazon* button in shop frontend
+ * Several improvements through the new *Login and pay with Amazon* services
+* **Image processing**: new processing and caching strategy! Thumbnails are not created synchronously during the main request anymore, instead a new middleware route defers processing until an image is requested by any client.
+* **TinyImage \***: scores ultra-high image compression rates (up to 80 %!) and enables WebP support.
+* **UrlRewriter \***: define URL redirection rules in the backend using *mod_rewrite* notation.
+* **Address formatting** templates by country
+* **Language packs**: downloader & auto-importer for packages available online.
* ...and a lot more new features, enhancements and fixes
\* Commercial plugin
@@ -105,4 +106,4 @@ NOTE: SmartStore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2
## License
-SmartStore.NET is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt).
+SmartStore.NET Community Edition is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt).
diff --git a/SmartStoreNET.Tasks.Targets b/SmartStoreNET.Tasks.Targets
index dd6a1a1895..fec484de68 100644
--- a/SmartStoreNET.Tasks.Targets
+++ b/SmartStoreNET.Tasks.Targets
@@ -39,7 +39,7 @@
x86
$(BUILD_NUMBER)
- 3.0.3
+ 3.1.5
$(StageFolder)
.$(Version)
diff --git a/changelog.md b/changelog.md
index 7b16444611..c29e6d43f7 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,32 +1,153 @@
# Release Notes
+## SmartStore.NET 3.2
+
+### New Features
+* #1144 Enable multi server search index
+* Made Topic ACL enabled
+* Implemented paging & filtering for Topic grid
+* Topics: added **IsPublished**, **Short Title** (link text) and **Intro** (teaser) properties.
+* New security option: Use invisible reCAPTCHA
+* **BeezUp**:
+ * #1459 Add option to only submit one category name per product
+ * Allow to specify export categories per product
+* Wallet: Allow customer to choose whether refund should be submitted to his wallet.
+* Added option to display preview pictures in product lists
+* Added option to add multiple file versions to product download section
+* Added options for alternating price display (in badges)
+* Added option to display a captcha on forum pages when creating or replying to a topic.
+* MegaSearch: Supports searching for forum posts.
+* Customer avatar: Letter with colored background if no avatar image was uploaded.
+
+### Improvements
+* (Perf) Significantly increased query performance for products with a lot of category assignments (> 10).
+* Debitoor: Partially update customer instead of full update to avoid all fields being overwritten
+* #1479 Show in messages the delivery time at the time of purchase
+
+### Bugfixes
+* In a multi-store environment, multiple topics with the same system name cannot be resolved reliably.
+* **GMC**:
+ * Export the product images if no attribute images are defined
+ * Do not export the first image twice for additional images
+ * Export image URL of full size image (not default size) for additional images
+* Media middleware: 0-byte files should be treated as missing.
+* Megamenu alpha/omega blends do not toggle correctly on touch devices
+* Summernote HTML editor exceeds parent container width when CodeMirror is activated
+* Only display a zero search hits warning if at least one filter is activated
+* #1436 Do not display delivery time in customer order completed messages
+* "ArgumentNullException: The value must not be NULL" if a topic is password protected
+* Tax by region: Fixes after inserting a tax rate country column shows "Unavailable"
+* #1014 Switching to default language keeps specific URL alias of current page
+* Shipping by total: Fixes when inserting a record the country was not saved
+* #1460 Editing of the customer title is missing on customer and address pages in the backend
+* #1447 Checkout button payment methods (Amazon, PayPal Express) won't work in conjunction with mandatory checkout attributes
+* When creating a topic, the widget zone input shows System.String[]
+* Switching the language always redirected to the home page if SEO friendly URLs was deactivated.
+* File upload of a checkout attribute was not stored on cart page.
+* Redirecting within checkout may have displayed an incorrect URL in the browser.
+* Server cannot modify cookies after HTTP headers have been sent.
+* Wrong base price on product and cart page when a special price is active.
+* In a multi-store, message templates may have loaded the wrong disclaimer and conditions-of-use text.
+* NullReferenceException in manufacturer list when there is no manufacturer.
+* Wrong order of featured products on category page.
+* #1504 Cart item price calculation wrong if attribute combinations with text types are involved.
+
+
+## SmartStore.NET 3.1.5
+
+### Highlights
+* Compliance with EU-GDPR requirements
+* Search engine friendly topic URLs
+* "Honeypot" bot detection for registration and contact forms.
+
+### New Features
+* #1429 Search engine friendly topic URLs
+* Implemented cookie consent according to EU-GDPR
+* Added checkboxes for data processing consent in all relevant forms
+* Implemented "Honeypot" bot detection for registration and contact forms.
+* Trusted Shops: Added consent checkbox to confirm order page for submission of customer email address to Trusted Shops if review widget is active
+* #1226 Shop-Connector: Added exchange of tier prices and delivery times
+* #1439 Debitoor: Option whether to display the payment method and SKU on invoices
+
+### Improvements
+* Added double opt-in feature for newsletter subscriptions during checkout (confirm order)
+* Allow forward slash in product tag URL slug
+* Theming: throttle AJAX cart refresh after spin up/down click
+* Moved StoreLastIpAddress & DisplayPrivacyAgreementOnContactUs from customer settings to privacy settings tab
+* #1450 Show the regular price only if it's higher than the final price
+* #1450 Do not ignore discounts with a negative amount
+* (Soft) deleted customers can be edited now
+* Customer IP addresses will be anonymized on (soft) deletion
+* Set catalogsettings.showsharebutton to false as its not compliant with GDPR
+* Made form fields for first & last name in customer registration optional
+* Implemented settings to make form fields for first & last name required again
+* Made form field for full name in contact us & product request optional
+* Implemented settings to make form field for full name in contact us & product request required again
+* #1453 Import: Use [IGNORE] to ignore a field value on record level
+* #1455 More detail on packing slip when bundled item
+* Display category alias as badge in grids and dropdowns
+
+### Bugfixes
+* Migration: take all same-named message templates into account
+* Messaging: OrderPlaced e-mail templates show main product image even when an attribute combination with a custom image has been selected
+* Theming: fix broken product review voting
+* Theming: added missing bottom space to .html-editor-content
+* Theming: Language switcher is not displayed if no currency options are available
+* No bundle item thumbnail displayed in bundle summary if item is not individually visible
+* Tracking number in shipment details was not saved
+* Assigning or removing product tags did not invalidate model cache
+* Reward points weren't displayed in message templates
+* Dashboard: link for not yet shipped orders loads list with all orders
+* Topic search button had no effect
+* #1442 Message factory should not throw an exception if a template has been deactivated
+* Fixes script error "$(...).tab is not a function" on product detail page
+* Title attribute for the product name in product lists was sometimes truncated
+* Relativizing font sizes should cast to double, not int
+* Fixes category list on product edit page shows empty category name
+* #1438 Debitoor: The country is displayed twice
+* MegaSearch: Fixes indexing ignores DeliveryTimeIdForEmptyStock setting
+* Web API: Fixes "No NavigationLink factory was found for the navigation property 'WalletHistory'"
+* #1449 IgnoreCharges of shipping methods is not working if a localized name is specified
+* Fixes "The object does not support the property or method 'startsWith'" on product edit page.
+* Wallet: Fixes "Child actions are not allowed to perform redirect actions" when there are cart warnings
+* Fixes the delivery time in the order notifications may differ from delivery time on the product detail page
+
+
## SmartStore.NET 3.1.0
-### Breaking changes
-* Message template customizations are lost due to the new template engine. You have to customize the templates again. No automatic migration, sorry :-(
-* Amazon Pay: The plugin has been changed to new *Login and pay with Amazon* services. A registration at Amazon and new access data are necessary for its use. The old access data can no longer be used.
-* (Dev) Calls to cache methods `Keys()` and `RemoveByPattern()` require glob chars to be present now (supported glob-styles see [https://redis.io/commands/keys](https://redis.io/commands/keys)). Previously these methods appended `*` to the passed pattern, which made pattern matching rather unflexible.
-* (Dev) Hook framework now passes `IHookedEntity` interface instead of `HookedEntity` class
-* (Dev) Completely removed all `EntityInserted`, `EntityUpdated` and `EntityDeleted` legacy events. We were using DbSaveHooks anyway, which provides a much more powerful and way faster pub-sub mechanism for database operations.
### Highlights
-* New [Liquid](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers) based template engine
-* Multi-configurable rounding of order total ("cash rounding"). Can be adjusted and activated separately for each currency and payment method.
-* (Perf) Picture service: new processing and caching strategy! Thumbnails are not created synchronously during the main request anymore, instead a new middleware route defers processing until an image is actually requested by any client.
-* MegaMenu shrinker and *Brands* virtual menu item
-* Address formatting templates by country
-* Connection to translate.smartstore.com. For available languages, localized resources can be downloaded and installed directly.
+* **Wallet**: Enables full or partial order payment via credit account. Includes REST-Api. (commercial plugin)
+* **[Liquid](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers) template engine**: very flexible templating for e-mails and campaigns with autocompletion and syntax highlighting.
+* **Cash Rounding**: define money rounding rules on currency and payment method level.
+* **Modern, responsive backend**: migrated backend to Bootstrap 4, overhauled and improved the user interface.
+* **Enhanced MegaMenu**: virtual dropdowns for surplus top-level categories and brands (commercial plugin exclusively bundled with Pro Edition).
+* **RTL**: comprehensive RTL (Right-to-left) and bidi(rectional) support.
* **Amazon Pay**:
* Supports merchants registered in the USA and Japan
* External authentication via *Login with Amazon* button in shop frontend
* Several improvements through the new *Login and pay with Amazon* services
+* (Perf) **Faster image processing**: new processing and caching strategy! Thumbnails are not created synchronously during the main request anymore, instead a new middleware route defers processing until an image is requested by any client.
+* **TinyImage**: scores ultra-high image compression rates (up to 80 %!) and enables WebP support (commercial plugin exclusively bundled with Premium Edition).
+* **UrlRewriter**: define URL redirection rules in the backend using *mod_rewrite* notation. (commercial plugin)
+* **Address formatting** templates by country
+* **Language packs**: downloader & auto-importer for packages available online.
+
+### Breaking changes
+* Message template customizations are lost due to the new template engine. You have to customize the templates again. No automatic migration, sorry :-(
+* Amazon Pay: The plugin has been changed to new *Login and pay with Amazon* services. The client ID has been added, which has to be created in Amazon Seller Central and saved in the payment method configuration.
+* (Dev) Calls to cache methods `Keys()` and `RemoveByPattern()` require glob chars to be present now (supported glob-styles see [https://redis.io/commands/keys](https://redis.io/commands/keys)). Previously these methods appended `*` to the passed pattern, which made pattern matching rather unflexible.
+* (Dev) Hook framework now passes `IHookedEntity` interface instead of `HookedEntity` class
+* (Dev) Completely removed all `EntityInserted`, `EntityUpdated` and `EntityDeleted` legacy events. We were using DbSaveHooks anyway, which provides a much more powerful and way faster pub-sub mechanism for database operations.
### New Features
* 1203 MegaMenu shrinker and *Brands* virtual menu item
+* [Summernote](https://summernote.org/) is now the primary HTML editor
* #431 Added option to randomize the display order for slides on each request
* #1258 Add option to filter shipping and payment methods by a specific customer role
* #1247 Allow to import non system customer roles in customer import
* #1117 Added an option to display a dropdown menu for manufacturers
* #1203 Added an option to define a maximum number of elements in the main menu for the first hierarchy of the catalog navigation
+* GMC: column chooser for edit grid
* #1100 Customer can register in frontend via "Login with Amazon" button
* **Web API**:
* #1292 Added endpoint to get order in PDF format
@@ -37,12 +158,18 @@
* #1295 Sales tracking (tracking pixel) for Billiger.de
* XML and CSV export of shopping cart and wishlist items
* #1363 Make storing of IP addresses optional
+* #729 Option for automatic order amount capturing when the shipping status changed to "shipped"
+* (Dev) ILocalizationFileResolver: responsible for finding localization files for client scripts
+* #998 GMC: Find a way to map attribute combination values to feed export values
+* Added Instagram icon to social media icons in footer
### Improvements
* Target .NET Framework changed: 4.5.2 > 4.6.1.
* Lower memory consumption
* #649 Media FileSystem provider: segmenting files in subfolders to increase IO perf with huge amount of files
-* #1141 Clearer backend order list. Added more infos like payment method.
+* #1141 Cleaner backend order list. Added more infos like payment method.
+* OuputCache: Simple product changes that affect visibility now correctly invalidate all assigned category and manufacturer pages
+* * OuputCache: When MegaSearch is active, invalidation occurs only during indexing and not ad-hoc anymore.
* #1248 New payment integration guidelines for Sofort\Klarna
* TwitterAuth: better error handling and enhanced admin instruction
* #1181 Debitoor: Add option to display shipping address on invoices
@@ -52,6 +179,12 @@
* (Perf) Many improvements in hooking framework
* #1294 Swiss PostFinance: External payment page too small on mobile devices. Added setting for mobile device template URL, pre-configured with PostFinance template.
* #1143 Make shipping methods suitable for multi-stores
+* #1320 Image import: Find out the content type of image URLs by response header rather than file extension (which is sometimes missing)
+* #1219 Recently viewed products list should respect setting to hide manufacturer names
+* Import and export product quantity step
+* Add bundle item information to order messages
+* #1031 Enable offline payment methods to have brand icons
+* DevTools Plugin: Added example for cached output invalidation
### Bugfixes
* #1268 Data importer always inserts new pictures and does not detect equal pictures while importing
@@ -71,7 +204,9 @@
* The tax value per tax rate was not updated when adding\removing a product to\from the order.
* The option to send manually was ignored when sending e-mails
* #528 LimitedToStores is required on payment provider rather than plugin level
-
+* #1318 Disabled preselected attribute combination permanently hides the shopping cart button, even if another combination is selected.
+* Copy product: Fixes "Cannot insert duplicate key row in object dbo.UrlRecord with unique index IX_UrlRecord_Slug"
+* Fixed export publishing via email
## SmartStore.NET 3.0.3
### Bugfixes
@@ -138,7 +273,9 @@
* MegaSearch: Localized labels of filters were never displayed
* #1195 Exporter: don't send an email if no email account has been selected
* Product lists sometimes show the wrong delivery time
-* #1192 Lucene indexing performance decreases the longer it takes
+* #1192 Lucene indexing
+*
+* decreases the longer it takes
* #1198 MegaSearch: never sort numeric range by label, always by value
* Filter for attributes were always sorted by hit count
* #1200 PayPal PLUS: Invalid request if the order amount is zero
@@ -172,7 +309,7 @@
* Added config setting *sm:PdfEngineBaseUrl*. There are cases where the PDF converter exits with a network error, when it is unable to load automatically resolved URLs.
* (Dev) Added *Retry* utility class
* #1176 Admin > Product Search: It ain't possible to search for parts of a product name
-
+
### Bugfixes
* #1145: Fixed HTTP 404 after switching language
* Fixed null reference exception in product lists if sorting is not allowed
diff --git a/src/AssemblyVersionInfo.cs b/src/AssemblyVersionInfo.cs
index 63fae0f3e7..d9e092d65c 100644
--- a/src/AssemblyVersionInfo.cs
+++ b/src/AssemblyVersionInfo.cs
@@ -9,7 +9,7 @@
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:
-[assembly: AssemblyVersion("3.0.3.0")]
+[assembly: AssemblyVersion("3.1.5.0")]
-[assembly: AssemblyFileVersion("3.0.3.0")]
-[assembly: AssemblyInformationalVersion("3.0.3.0")]
+[assembly: AssemblyFileVersion("3.1.5.0")]
+[assembly: AssemblyInformationalVersion("3.1.5.0")]
diff --git a/src/Libraries/SmartStore.Core/BaseEntity.cs b/src/Libraries/SmartStore.Core/BaseEntity.cs
index c649970d6d..b591189f8d 100644
--- a/src/Libraries/SmartStore.Core/BaseEntity.cs
+++ b/src/Libraries/SmartStore.Core/BaseEntity.cs
@@ -6,8 +6,7 @@
using System.Runtime.Serialization;
namespace SmartStore.Core
-{
-
+{
///
/// Base class for entities
///
diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs
index ec6fc43daf..9629fee3c5 100644
--- a/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs
+++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs
@@ -1,57 +1,185 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using Autofac;
using SmartStore.Core.Data;
using SmartStore.Core.Domain.Blogs;
using SmartStore.Core.Domain.Catalog;
using SmartStore.Core.Domain.Discounts;
+using SmartStore.Core.Domain.Localization;
using SmartStore.Core.Domain.News;
using SmartStore.Core.Domain.Topics;
using SmartStore.Utilities;
namespace SmartStore.Core.Caching
{
+ public delegate IEnumerable DisplayControlHandler(BaseEntity entity, IComponentContext ctx);
+
public partial class DisplayControl : IDisplayControl
{
- public static readonly HashSet CandidateTypes = new HashSet(new Type[]
+ private static readonly ConcurrentDictionary _handlers = new ConcurrentDictionary
{
- typeof(BlogComment),
- typeof(BlogPost),
- typeof(Category),
- typeof(Manufacturer),
- typeof(Product),
- typeof(ProductBundleItem),
- typeof(ProductPicture),
- typeof(SpecificationAttribute),
- typeof(ProductSpecificationAttribute),
- typeof(SpecificationAttributeOption),
- typeof(ProductVariantAttribute),
- typeof(ProductVariantAttributeValue),
- typeof(ProductVariantAttributeCombination),
- typeof(TierPrice),
- typeof(Discount),
- typeof(CrossSellProduct),
- typeof(RelatedProduct),
- typeof(ProductCategory),
- typeof(ProductManufacturer),
- typeof(NewsItem),
- typeof(NewsComment),
- typeof(Topic)
- });
+ [typeof(BlogComment)] = (x, c) => new[] { "b" + ((BlogComment)x).BlogPostId },
+ [typeof(BlogPost)] = (x, c) => new[] { "b" + x.Id },
+ [typeof(Category)] = (x, c) => new[] { "c" + x.Id },
+ [typeof(Manufacturer)] = (x, c) => new[] { "m" + x.Id },
+ [typeof(ProductBundleItem)] = (x, c) => new[] { "p" + ((ProductBundleItem)x).ProductId },
+ [typeof(ProductPicture)] = (x, c) => new[] { "p" + ((ProductPicture)x).ProductId },
+ [typeof(ProductSpecificationAttribute)] = (x, c) => new[] { "p" + ((ProductSpecificationAttribute)x).ProductId },
+ [typeof(ProductVariantAttributeCombination)]= (x, c) => new[] { "p" + ((ProductVariantAttributeCombination)x).ProductId },
+ [typeof(TierPrice)] = (x, c) => new[] { "p" + ((TierPrice)x).ProductId },
+ [typeof(CrossSellProduct)] = (x, c) => new[] { "p" + ((CrossSellProduct)x).ProductId1, "p" + ((CrossSellProduct)x).ProductId2 },
+ [typeof(RelatedProduct)] = (x, c) => new[] { "p" + ((RelatedProduct)x).ProductId1, "p" + ((RelatedProduct)x).ProductId2 },
+ [typeof(ProductCategory)] = (x, c) => new[] { "p" + ((ProductCategory)x).CategoryId, "p" + ((ProductCategory)x).ProductId },
+ [typeof(ProductManufacturer)] = (x, c) => new[] { "p" + ((ProductManufacturer)x).ManufacturerId, "p" + ((ProductManufacturer)x).ProductId },
+ [typeof(NewsItem)] = (x, c) => new[] { "n" + x.Id },
+ [typeof(NewsComment)] = (x, c) => new[] { "n" + ((NewsComment)x).NewsItemId },
+ [typeof(Topic)] = (x, c) => new[] { "t" + x.Id },
+ [typeof(SpecificationAttributeOption)] = (x, c) => ((SpecificationAttributeOption)x).ProductSpecificationAttributes.Select(y => "p" + y.ProductId),
+ [typeof(ProductTag)] = (x, c) => ((ProductTag)x).Products.Select(y => "p" + y.Id),
+ [typeof(Product)] = HandleProduct,
+ [typeof(SpecificationAttribute)] = HandleSpecificationAttribute,
+ [typeof(ProductVariantAttributeValue)] = HandleProductVariantAttributeValue,
+ [typeof(Discount)] = HandleDiscount,
+ [typeof(LocalizedProperty)] = HandleLocalizedProperty
+ };
private readonly HashSet _entities = new HashSet();
- private readonly Lazy> _rsProductSpecAttr;
+ private readonly IComponentContext _componentContext;
private bool _isIdle;
private bool? _isUncacheableRequest;
- public DisplayControl(Lazy> rsProductSpecAttr)
+ public DisplayControl(IComponentContext componentContext)
+ {
+ _componentContext = componentContext;
+ }
+
+ #region Static
+
+ public static bool ContainsHandlerFor(Type type)
+ {
+ Guard.NotNull(type, nameof(type));
+
+ return _handlers.ContainsKey(type);
+ }
+
+ public static void RegisterHandlerFor(Type type, DisplayControlHandler handler)
+ {
+ Guard.NotNull(type, nameof(type));
+ Guard.NotNull(handler, nameof(handler));
+
+ _handlers.TryAdd(type, handler);
+ }
+
+ #endregion
+
+ #region Handlers
+
+ private static IEnumerable HandleProduct(BaseEntity entity, IComponentContext ctx)
+ {
+ var product = ((Product)entity);
+ yield return "p" + entity.Id;
+ if (product.ProductType == ProductType.GroupedProduct && product.ParentGroupedProductId > 0)
+ {
+ yield return "p" + product.ParentGroupedProductId;
+ }
+ }
+
+ private static IEnumerable HandleSpecificationAttribute(BaseEntity entity, IComponentContext ctx)
+ {
+ // Determine all affected products (which are assigned to this attribute).
+ var specAttrId = ((SpecificationAttribute)entity).Id;
+ var affectedProductIds = ctx.Resolve().Set().AsNoTracking()
+ .Where(x => x.SpecificationAttributeOption.SpecificationAttribute.Id == specAttrId)
+ .Select(x => x.ProductId)
+ .Distinct()
+ .ToList();
+
+ foreach (var id in affectedProductIds)
+ {
+ yield return "p" + id;
+ }
+ }
+
+ private static IEnumerable HandleProductVariantAttributeValue(BaseEntity entity, IComponentContext ctx)
{
- _rsProductSpecAttr = rsProductSpecAttr;
+ var pva = ((ProductVariantAttributeValue)entity).ProductVariantAttribute;
+ if (pva != null)
+ {
+ yield return "p" + pva.ProductId;
+ }
+ }
+
+ private static IEnumerable HandleDiscount(BaseEntity entity, IComponentContext ctx)
+ {
+ var discount = (Discount)entity;
+ if (discount.DiscountType == DiscountType.AssignedToCategories)
+ {
+ foreach (var category in discount.AppliedToCategories)
+ {
+ yield return "c" + category.Id;
+ }
+ }
+ else if (discount.DiscountType == DiscountType.AssignedToSkus)
+ {
+ foreach (var product in discount.AppliedToProducts)
+ {
+ yield return "p" + product.Id;
+ }
+ }
}
+ private static IEnumerable HandleLocalizedProperty(BaseEntity entity, IComponentContext ctx)
+ {
+ var lp = (LocalizedProperty)entity;
+ string prefix = null;
+ BaseEntity targetEntity = null;
+
+ var dbContext = ctx.Resolve();
+
+ switch (lp.LocaleKeyGroup)
+ {
+ case nameof(Product):
+ prefix = "p";
+ break;
+ case nameof(Category):
+ prefix = "c";
+ break;
+ case nameof(Manufacturer):
+ prefix = "m";
+ break;
+ case nameof(Topic):
+ prefix = "t";
+ break;
+ case nameof(SpecificationAttribute):
+ targetEntity = dbContext.Set().Find(lp.EntityId);
+ break;
+ case nameof(SpecificationAttributeOption):
+ targetEntity = dbContext.Set().Find(lp.EntityId);
+ break;
+ case nameof(ProductVariantAttributeValue):
+ targetEntity = dbContext.Set().Find(lp.EntityId);
+ break;
+ }
+
+ if (prefix.HasValue())
+ {
+ yield return prefix + lp.EntityId;
+ }
+ else if (targetEntity != null)
+ {
+ var tags = ctx.Resolve().GetCacheControlTagsFor(targetEntity);
+ foreach (var tag in tags)
+ {
+ yield return tag;
+ }
+ }
+ }
+
+ #endregion
+
public IDisposable BeginIdleScope()
{
_isIdle = true;
@@ -91,157 +219,21 @@ public bool IsUncacheableRequest
public virtual IEnumerable GetCacheControlTagsFor(BaseEntity entity)
{
- Guard.NotNull(entity, nameof(entity));
+ var empty = Enumerable.Empty();
- if (entity.IsTransientRecord())
+ if (entity == null || entity.IsTransientRecord())
{
- yield break;
+ return empty;
}
var type = entity.GetUnproxiedType();
- if (!CandidateTypes.Contains(type))
+ if (!_handlers.TryGetValue(type, out var handler))
{
- yield break;
+ return empty;
}
- if (type == typeof(BlogComment))
- {
- yield return "b" + ((BlogComment)entity).BlogPostId;
- }
- else if (type == typeof(BlogPost))
- {
- yield return "b" + entity.Id;
- }
- else if (type == typeof(Category))
- {
- yield return "c" + entity.Id;
- }
- else if (type == typeof(Manufacturer))
- {
- yield return "m" + entity.Id;
- }
- else if (type == typeof(Product))
- {
- var product = ((Product)entity);
- yield return "p" + entity.Id;
- if (product.ProductType == ProductType.GroupedProduct && product.ParentGroupedProductId > 0)
- {
- yield return "p" + product.ParentGroupedProductId;
- }
- }
- else if (type == typeof(ProductTag))
- {
- var ids = ((ProductTag)entity).Products.Select(x => x.Id);
- foreach (var id in ids)
- {
- yield return "p" + id;
- }
- }
- else if (type == typeof(ProductBundleItem))
- {
- yield return "p" + ((ProductBundleItem)entity).ProductId;
- }
- else if (type == typeof(ProductPicture))
- {
- yield return "p" + ((ProductPicture)entity).ProductId;
- }
- else if (type == typeof(SpecificationAttribute))
- {
- // Determine all affected products (which are assigned to this attribute).
- var specAttrId = ((SpecificationAttribute)entity).Id;
- var affectedProductIds = _rsProductSpecAttr.Value.TableUntracked
- .Where(x => x.SpecificationAttributeOption.SpecificationAttribute.Id == specAttrId)
- .Select(x => x.ProductId)
- .Distinct()
- .ToList();
-
- foreach (var id in affectedProductIds)
- {
- yield return "p" + id;
- }
- }
- else if (type == typeof(ProductSpecificationAttribute))
- {
- yield return "p" + ((ProductSpecificationAttribute)entity).ProductId;
- }
- else if (type == typeof(SpecificationAttributeOption))
- {
- foreach (var attr in ((SpecificationAttributeOption)entity).ProductSpecificationAttributes)
- {
- yield return "p" + attr.ProductId;
- }
- }
- else if (type == typeof(ProductVariantAttribute))
- {
- yield return "p" + ((ProductVariantAttribute)entity).ProductId;
- }
- else if (type == typeof(ProductVariantAttributeValue))
- {
- var pva = ((ProductVariantAttributeValue)entity).ProductVariantAttribute;
- if (pva != null)
- {
- yield return "p" + pva.ProductId;
- }
- }
- else if (type == typeof(ProductVariantAttributeCombination))
- {
- yield return "p" + ((ProductVariantAttributeCombination)entity).ProductId;
- }
- else if (type == typeof(TierPrice))
- {
- yield return "p" + ((TierPrice)entity).ProductId;
- }
- else if (type == typeof(Discount))
- {
- var discount = (Discount)entity;
- if (discount.DiscountType == DiscountType.AssignedToCategories)
- {
- foreach (var category in discount.AppliedToCategories)
- {
- yield return "c" + category.Id;
- }
- }
- else if (discount.DiscountType == DiscountType.AssignedToSkus)
- {
- foreach (var product in discount.AppliedToProducts)
- {
- yield return "p" + product.Id;
- }
- }
- }
- else if (type == typeof(CrossSellProduct))
- {
- yield return "p" + ((CrossSellProduct)entity).ProductId1;
- yield return "p" + ((CrossSellProduct)entity).ProductId2;
- }
- else if (type == typeof(RelatedProduct))
- {
- yield return "p" + ((RelatedProduct)entity).ProductId1;
- yield return "p" + ((RelatedProduct)entity).ProductId2;
- }
- else if (type == typeof(ProductCategory))
- {
- yield return "c" + ((ProductCategory)entity).CategoryId;
- yield return "p" + ((ProductCategory)entity).ProductId;
- }
- else if (type == typeof(ProductManufacturer))
- {
- yield return "m" + ((ProductManufacturer)entity).ManufacturerId;
- yield return "p" + ((ProductManufacturer)entity).ProductId;
- }
- else if (type == typeof(NewsItem))
- {
- yield return "n" + entity.Id;
- }
- else if (type == typeof(NewsComment))
- {
- yield return "n" + ((NewsComment)entity).NewsItemId;
- }
- else if (type == typeof(Topic))
- {
- yield return "t" + entity.Id;
- }
+ return handler.Invoke(entity, _componentContext);
}
public IEnumerable GetAllCacheControlTags()
diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs
index 6542f2a4f6..234b67ced2 100644
--- a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs
+++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs
@@ -1,11 +1,9 @@
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
-using Autofac;
using SmartStore.Core.Configuration;
-using SmartStore.Core.Data;
+using SmartStore.Core.Data.Hooks;
using SmartStore.Core.Infrastructure.DependencyManagement;
using SmartStore.Utilities;
@@ -14,8 +12,9 @@ namespace SmartStore.Core.Caching
public class ObserveEntityContext
{
public IOutputCacheProvider OutputCacheProvider { get; set; }
+ public IDisplayControl DisplayControl { get; set; }
public BaseEntity Entity { get; set; }
- public EntityState EntityState { get; set; }
+ public IHookedEntity EntityEntry { get; set; }
public bool Handled { get; set; }
public ContainerManager ServiceContainer { get; set; }
}
diff --git a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs
index d9e6c700b6..0e02723ec4 100644
--- a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs
+++ b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
-using System.Linq.Expressions;
using Newtonsoft.Json;
namespace SmartStore.Collections
diff --git a/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs b/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs
index 6d81d0bc3a..114c4e24bf 100644
--- a/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs
+++ b/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs
@@ -42,7 +42,7 @@ public MostRecentlyUsedList(IEnumerable collection, T newItem, int maxSize)
public MostRecentlyUsedList(string collection, T newItem, int maxSize)
{
_maxSize = maxSize;
- _mru = collection.SplitSafe(Delimiter).Cast().ToList();
+ _mru = collection.SplitSafe(Delimiter).Cast().Distinct().ToList();
Add(newItem);
}
diff --git a/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs b/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs
new file mode 100644
index 0000000000..b70b4302d2
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace SmartStore.ComponentModel
+{
+ public sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer
///
- /// the Date to round, if the current time will
+ /// the Date to round, if the current time will
/// be used
/// the new rounded date
- public static DateTime GetEvenHourDate(this DateTime? dateTime)
+ public static DateTime GetEvenHourDate(this DateTime? value)
{
- if (!dateTime.HasValue)
+ if (!value.HasValue)
{
- dateTime = DateTime.UtcNow;
+ value = DateTime.UtcNow;
}
- DateTime d = dateTime.Value.AddHours(1);
+ DateTime d = value.Value.AddHours(1);
return new DateTime(d.Year, d.Month, d.Day, d.Hour, 0, 0);
}
@@ -61,16 +62,16 @@ public static DateTime GetEvenHourDate(this DateTime? dateTime)
/// then the hour (and possibly the day) will be promoted.
///
///
- /// The Date to round, if the current time will be used
+ /// The Date to round, if the current time will be used
/// The new rounded date
- public static DateTime GetEvenMinuteDate(this DateTime? dateTime)
+ public static DateTime GetEvenMinuteDate(this DateTime? value)
{
- if (!dateTime.HasValue)
+ if (!value.HasValue)
{
- dateTime = DateTime.UtcNow;
+ value = DateTime.UtcNow;
}
- DateTime d = dateTime.Value;
+ DateTime d = value.Value;
d = d.AddMinutes(1);
return new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, 0);
}
@@ -83,23 +84,23 @@ public static DateTime GetEvenMinuteDate(this DateTime? dateTime)
/// with the time of 08:13:00.
///
///
- /// the Date to round, if the current time will
+ /// the Date to round, if the current time will
/// be used
/// the new rounded date
- public static DateTime GetEvenMinuteDateBefore(this DateTime? dateTime)
+ public static DateTime GetEvenMinuteDateBefore(this DateTime? value)
{
- if (!dateTime.HasValue)
+ if (!value.HasValue)
{
- dateTime = DateTime.UtcNow;
+ value = DateTime.UtcNow;
}
- DateTime d = dateTime.Value;
+ DateTime d = value.Value;
return new DateTime(d.Year, d.Month, d.Day, d.Hour, d.Minute, 0);
}
- public static long ToJavaScriptTicks(this DateTime dateTime)
+ public static long ToJavaScriptTicks(this DateTime value)
{
- DateTimeOffset utcDateTime = dateTime.ToUniversalTime();
+ DateTimeOffset utcDateTime = value.ToUniversalTime();
long javaScriptTicks = (utcDateTime.Ticks - BeginOfEpoch.Ticks) / (long)10000;
return javaScriptTicks;
}
@@ -108,11 +109,11 @@ public static long ToJavaScriptTicks(this DateTime dateTime)
/// Get the first day of the month for
/// any full date submitted
///
- ///
+ ///
///
- public static DateTime GetFirstDayOfMonth(this DateTime date)
+ public static DateTime GetFirstDayOfMonth(this DateTime value)
{
- DateTime dtFrom = date;
+ DateTime dtFrom = value;
dtFrom = dtFrom.AddDays(-(dtFrom.Day - 1));
return dtFrom;
}
@@ -121,39 +122,76 @@ public static DateTime GetFirstDayOfMonth(this DateTime date)
/// Get the last day of the month for any
/// full date
///
- ///
+ ///
///
- public static DateTime GetLastDayOfMonth(this DateTime date)
+ public static DateTime GetLastDayOfMonth(this DateTime value)
{
- DateTime dtTo = date;
+ DateTime dtTo = value;
dtTo = dtTo.AddMonths(1);
dtTo = dtTo.AddDays(-(dtTo.Day));
return dtTo;
}
- public static DateTime ToEndOfTheDay(this DateTime dt)
+ public static DateTime ToEndOfTheDay(this DateTime value)
{
- if (dt != null)
- return new DateTime(dt.Year, dt.Month, dt.Day, 23, 59, 59);
- return dt;
+ if (value != null)
+ return new DateTime(value.Year, value.Month, value.Day, 23, 59, 59);
+ return value;
}
- public static DateTime? ToEndOfTheDay(this DateTime? dt)
+ public static DateTime? ToEndOfTheDay(this DateTime? value)
{
- return (dt.HasValue ? dt.Value.ToEndOfTheDay() : dt);
+ return (value.HasValue ? value.Value.ToEndOfTheDay() : value);
}
- /// Epoch time. Number of seconds since midnight (UTC) on 1st January 1970.
- public static long ToUnixTime(this DateTime date)
+ ///
+ /// Epoch time. Number of seconds since midnight (UTC) on 1st January 1970.
+ ///
+ public static long ToUnixTime(this DateTime value)
{
- return Convert.ToInt64((date.ToUniversalTime() - BeginOfEpoch).TotalSeconds);
+ return Convert.ToInt64((value.ToUniversalTime() - BeginOfEpoch).TotalSeconds);
}
- /// UTC date based on number of seconds since midnight (UTC) on 1st January 1970.
+ ///
+ /// UTC date based on number of seconds since midnight (UTC) on 1st January 1970.
+ ///
public static DateTime FromUnixTime(this long unixTime)
{
return BeginOfEpoch.AddSeconds(unixTime);
}
+
+ ///
+ /// Converts a DateTime to a string with native digits
+ ///
+ public static string ToNativeString(this DateTime value)
+ {
+ return value.ToNativeString(null, null);
+ }
+
+ ///
+ /// Converts a DateTime to a string with native digits
+ ///
+ public static string ToNativeString(this DateTime value, IFormatProvider provider)
+ {
+ return value.ToNativeString(null, provider);
+ }
+
+ ///
+ /// Converts a DateTime to a string with native digits
+ ///
+ public static string ToNativeString(this DateTime value, string format)
+ {
+ return value.ToNativeString(format, null);
+ }
+
+ ///
+ /// Converts a DateTime to a string with native digits
+ ///
+ public static string ToNativeString(this DateTime value, string format, IFormatProvider provider)
+ {
+ provider = provider ?? CultureInfo.CurrentCulture;
+ return value.ToString(format, provider).ReplaceNativeDigits(provider);
+ }
}
}
diff --git a/src/Libraries/SmartStore.Core/Extensions/DecimalExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/DecimalExtensions.cs
deleted file mode 100644
index 83e67555a0..0000000000
--- a/src/Libraries/SmartStore.Core/Extensions/DecimalExtensions.cs
+++ /dev/null
@@ -1,139 +0,0 @@
-using System;
-using System.Globalization;
-using SmartStore.Core.Domain.Directory;
-
-namespace SmartStore
-{
- public static class DecimalExtensions
- {
- ///
- /// Calculates the tax (percentage) from a gross and a net value.
- ///
- /// Gross value
- /// Net value
- /// Rounding decimal number
- /// Tax percentage
- public static decimal ToTaxPercentage(this decimal inclTax, decimal exclTax, int? decimals = null)
- {
- if (exclTax == decimal.Zero)
- {
- return decimal.Zero;
- }
-
- var result = ((inclTax / exclTax) - 1.0M) * 100.0M;
-
- return (decimals.HasValue ? Math.Round(result, decimals.Value) : result);
- }
-
- ///
- /// Converts to smallest currency uint, e.g. cents
- ///
- /// Handling of the midway between two numbers. "ToEven" round down, "AwayFromZero" round up.
- /// Smallest currency unit
- public static int ToSmallestCurrencyUnit(this decimal value, MidpointRounding midpoint = MidpointRounding.AwayFromZero)
- {
- var result = Math.Round(value * 100, 0, midpoint);
- return Convert.ToInt32(result);
- }
-
- ///
- /// Round decimal to the nearest multiple of denomination
- ///
- /// Value to round
- /// Denomination
- /// Handling of the midway between two numbers. "ToEven" round down, "AwayFromZero" round up.
- /// Rounded value
- public static decimal RoundToNearest(this decimal value, decimal denomination, MidpointRounding midpoint = MidpointRounding.AwayFromZero)
- {
- if (denomination == decimal.Zero)
- {
- return value;
- }
-
- return Math.Round(value / denomination, midpoint) * denomination;
- }
-
- ///
- /// Round decimal up or down to the nearest multiple of denomination
- ///
- /// Value to round
- /// Denomination
- /// true round to, false round down
- /// Rounded value
- public static decimal RoundToNearest(this decimal value, decimal denomination, bool roundUp)
- {
- if (denomination == decimal.Zero)
- {
- return value;
- }
-
- var roundedValueBase = roundUp
- ? Math.Ceiling(value / denomination)
- : Math.Floor(value / denomination);
-
- return Math.Round(roundedValueBase) * denomination;
- }
-
- ///
- /// Round decimal up or down to the nearest multiple of denomination if activated for currency
- ///
- /// Value to round
- /// Currency. Rounding must be activated for this currency.
- /// The rounding amount
- /// Rounded value
- public static decimal RoundToNearest(this decimal value, Currency currency, out decimal roundingAmount)
- {
- var oldValue = value;
-
- switch (currency.RoundOrderTotalRule)
- {
- case CurrencyRoundingRule.RoundMidpointUp:
- value = value.RoundToNearest(currency.RoundOrderTotalDenominator, MidpointRounding.AwayFromZero);
- break;
- case CurrencyRoundingRule.AlwaysRoundDown:
- value = value.RoundToNearest(currency.RoundOrderTotalDenominator, false);
- break;
- case CurrencyRoundingRule.AlwaysRoundUp:
- value = value.RoundToNearest(currency.RoundOrderTotalDenominator, true);
- break;
- case CurrencyRoundingRule.RoundMidpointDown:
- default:
- value = value.RoundToNearest(currency.RoundOrderTotalDenominator, MidpointRounding.ToEven);
- break;
- }
-
- roundingAmount = value - Math.Round(oldValue, 2);
-
- return value;
- }
-
- ///
- /// Rounds a value if rounding is enabled for the currency
- ///
- /// Value to round
- /// Currency
- /// Rounded value
- public static decimal RoundIfEnabledFor(this decimal value, Currency currency)
- {
- Guard.NotNull(currency, nameof(currency));
-
- if (currency.RoundOrderItemsEnabled)
- {
- return Math.Round(value, currency.RoundNumDecimals);
- }
-
- return value;
- }
-
- ///
- /// Rounds and formats a decimal culture invariant
- ///
- /// Value to round
- /// Rounding decimal number
- /// Rounded and formated value
- public static string FormatInvariant(this decimal value, int decimals = 2)
- {
- return Math.Round(value, decimals).ToString("0.00", CultureInfo.InvariantCulture);
- }
- }
-}
diff --git a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs
index 87abdd2e99..b3b7be8905 100644
--- a/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/EnumerableExtensions.cs
@@ -286,6 +286,11 @@ join entity in source on id equals entity.Id
return sorted;
}
+ public static string StrJoin(this IEnumerable source, string separator)
+ {
+ return string.Join(separator, source);
+ }
+
#endregion
#region Multimap
@@ -295,25 +300,34 @@ public static Multimap ToMultimap(
Func keySelector,
Func valueSelector)
{
- Guard.NotNull(source, nameof(source));
- Guard.NotNull(keySelector, nameof(keySelector));
- Guard.NotNull(valueSelector, nameof(valueSelector));
+ return source.ToMultimap(keySelector, valueSelector, null);
+ }
- var map = new Multimap();
+ public static Multimap ToMultimap(
+ this IEnumerable source,
+ Func keySelector,
+ Func valueSelector,
+ IEqualityComparer comparer)
+ {
+ Guard.NotNull(source, nameof(source));
+ Guard.NotNull(keySelector, nameof(keySelector));
+ Guard.NotNull(valueSelector, nameof(valueSelector));
- foreach (var item in source)
- {
- map.Add(keySelector(item), valueSelector(item));
- }
+ var map = new Multimap(comparer);
- return map;
- }
+ foreach (var item in source)
+ {
+ map.Add(keySelector(item), valueSelector(item));
+ }
- #endregion
+ return map;
+ }
+
+ #endregion
- #region NameValueCollection
+ #region NameValueCollection
- public static void AddRange(this NameValueCollection initial, NameValueCollection other)
+ public static void AddRange(this NameValueCollection initial, NameValueCollection other)
{
Guard.NotNull(initial, "initial");
diff --git a/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs
index 7810a19b12..8dfa07887e 100644
--- a/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/HttpExtensions.cs
@@ -18,6 +18,7 @@ public static class HttpExtensions
{
private const string HTTP_CLUSTER_VAR = "HTTP_CLUSTER_HTTPS";
private const string HTTP_XFWDPROTO_VAR = "HTTP_X_FORWARDED_PROTO";
+ private const string CACHE_REGION_NAME = "SmartStoreNET:";
///
/// Gets a value which indicates whether the HTTP connection uses secure sockets (HTTPS protocol).
@@ -140,7 +141,7 @@ private static void CopyCookie(HttpWebRequest webRequest, HttpRequestBase source
public static string BuildScopedKey(this Cache cache, string key)
{
- return key.HasValue() ? "SmartStoreNET:" + key : null;
+ return key.HasValue() ? CACHE_REGION_NAME + key : null;
}
public static T GetOrAdd(this Cache cache, string key, Func acquirer, TimeSpan? duration = null)
@@ -170,11 +171,6 @@ public static T GetOrAdd(this Cache cache, string key, Func acquirer, Time
public static T GetItem(this HttpContext httpContext, string key, Func factory = null, bool forceCreation = true)
{
- if (httpContext?.Items == null)
- {
- return default(T);
- }
-
return GetItem(new HttpContextWrapper(httpContext), key, factory, forceCreation);
}
@@ -208,22 +204,27 @@ public static T GetItem(this HttpContextBase httpContext, string key, Func
public static void RemoveByPattern(this Cache cache, string pattern)
{
- var regionName = "SmartStoreNET:";
+ var keys = cache.AllKeys(pattern);
- pattern = pattern == "*" ? regionName : pattern;
+ foreach (var key in keys.ToArray())
+ {
+ cache.Remove(key);
+ }
+ }
+
+ public static string[] AllKeys(this Cache cache, string pattern)
+ {
+ pattern = pattern == "*" ? CACHE_REGION_NAME : pattern;
var keys = from entry in HttpRuntime.Cache.AsParallel().Cast()
let key = entry.Key.ToString()
where key.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)
select key;
- foreach (var key in keys.ToArray())
- {
- cache.Remove(key);
- }
+ return keys.ToArray();
}
- public static ControllerContext GetMasterControllerContext(this ControllerContext controllerContext)
+ public static ControllerContext GetMasterControllerContext(this ControllerContext controllerContext)
{
Guard.NotNull(controllerContext, nameof(controllerContext));
diff --git a/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs
index d176d04adb..5b6c628c10 100644
--- a/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/IOExtensions.cs
@@ -8,8 +8,7 @@
namespace SmartStore
{
public static class IOExtensions
- {
-
+ {
public static bool IsFileLocked(this FileInfo file)
{
if (file == null)
@@ -23,10 +22,10 @@ public static bool IsFileLocked(this FileInfo file)
}
catch (IOException)
{
- //the file is unavailable because it is:
- //still being written to
- //or being processed by another thread
- //or does not exist (has already been processed)
+ // the file is unavailable because it is:
+ // still being written to
+ // or being processed by another thread
+ // or does not exist (has already been processed)
return true;
}
finally
@@ -39,5 +38,4 @@ public static bool IsFileLocked(this FileInfo file)
return false;
}
}
-
}
diff --git a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs
index f4452fb5ed..d687cd92b1 100644
--- a/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/MiscExtensions.cs
@@ -1,11 +1,6 @@
using System;
-using System.ComponentModel;
-using System.Data;
-using System.Data.OleDb;
using System.Diagnostics;
using System.Text;
-using System.Web.Routing;
-using SmartStore.Core;
namespace SmartStore
{
@@ -51,7 +46,9 @@ public static bool IsNullOrDefault(this T? value) where T : struct
return default(T).Equals(value.GetValueOrDefault());
}
- /// Converts bytes into a hex string.
+ ///
+ /// Converts bytes into a hex string.
+ ///
public static string ToHexString(this byte[] bytes, int length = 0)
{
if (bytes == null || bytes.Length <= 0)
diff --git a/src/Libraries/SmartStore.Core/Extensions/NumericExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/NumericExtensions.cs
new file mode 100644
index 0000000000..ebaccbd53b
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Extensions/NumericExtensions.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Globalization;
+using SmartStore.Core.Domain.Directory;
+
+namespace SmartStore
+{
+ public static class NumericExtensions
+ {
+ #region int
+
+ public static int GetRange(this int id, int size, out int lower)
+ {
+ lower = 0;
+
+ // max 1000 values per cache item
+ var range = (int)Math.Ceiling((decimal)id / size) * size;
+
+ lower = range - (size - 1);
+
+ return range;
+ }
+
+ #endregion
+
+ #region decimal
+
+ ///
+ /// Calculates the tax (percentage) from a gross and a net value.
+ ///
+ /// Gross value
+ /// Net value
+ /// Rounding decimal number
+ /// Tax percentage
+ public static decimal ToTaxPercentage(this decimal inclTax, decimal exclTax, int? decimals = null)
+ {
+ if (exclTax == decimal.Zero)
+ {
+ return decimal.Zero;
+ }
+
+ var result = ((inclTax / exclTax) - 1.0M) * 100.0M;
+
+ return (decimals.HasValue ? Math.Round(result, decimals.Value) : result);
+ }
+
+ ///
+ /// Converts to smallest currency uint, e.g. cents
+ ///
+ /// Handling of the midway between two numbers. "ToEven" round down, "AwayFromZero" round up.
+ /// Smallest currency unit
+ public static int ToSmallestCurrencyUnit(this decimal value, MidpointRounding midpoint = MidpointRounding.AwayFromZero)
+ {
+ var result = Math.Round(value * 100, 0, midpoint);
+ return Convert.ToInt32(result);
+ }
+
+ ///
+ /// Round decimal to the nearest multiple of denomination
+ ///
+ /// Value to round
+ /// Denomination
+ /// Handling of the midway between two numbers. "ToEven" round down, "AwayFromZero" round up.
+ /// Rounded value
+ public static decimal RoundToNearest(this decimal value, decimal denomination, MidpointRounding midpoint = MidpointRounding.AwayFromZero)
+ {
+ if (denomination == decimal.Zero)
+ {
+ return value;
+ }
+
+ return Math.Round(value / denomination, midpoint) * denomination;
+ }
+
+ ///
+ /// Round decimal up or down to the nearest multiple of denomination
+ ///
+ /// Value to round
+ /// Denomination
+ /// true round to, false round down
+ /// Rounded value
+ public static decimal RoundToNearest(this decimal value, decimal denomination, bool roundUp)
+ {
+ if (denomination == decimal.Zero)
+ {
+ return value;
+ }
+
+ var roundedValueBase = roundUp
+ ? Math.Ceiling(value / denomination)
+ : Math.Floor(value / denomination);
+
+ return Math.Round(roundedValueBase) * denomination;
+ }
+
+ ///
+ /// Round decimal up or down to the nearest multiple of denomination if activated for currency
+ ///
+ /// Value to round
+ /// Currency. Rounding must be activated for this currency.
+ /// The rounding amount
+ /// Rounded value
+ public static decimal RoundToNearest(this decimal value, Currency currency, out decimal roundingAmount)
+ {
+ var oldValue = value;
+
+ switch (currency.RoundOrderTotalRule)
+ {
+ case CurrencyRoundingRule.RoundMidpointUp:
+ value = value.RoundToNearest(currency.RoundOrderTotalDenominator, MidpointRounding.AwayFromZero);
+ break;
+ case CurrencyRoundingRule.AlwaysRoundDown:
+ value = value.RoundToNearest(currency.RoundOrderTotalDenominator, false);
+ break;
+ case CurrencyRoundingRule.AlwaysRoundUp:
+ value = value.RoundToNearest(currency.RoundOrderTotalDenominator, true);
+ break;
+ case CurrencyRoundingRule.RoundMidpointDown:
+ default:
+ value = value.RoundToNearest(currency.RoundOrderTotalDenominator, MidpointRounding.ToEven);
+ break;
+ }
+
+ roundingAmount = value - Math.Round(oldValue, 2);
+
+ return value;
+ }
+
+ ///
+ /// Rounds a value if rounding is enabled for the currency
+ ///
+ /// Value to round
+ /// Currency
+ /// Rounded value
+ public static decimal RoundIfEnabledFor(this decimal value, Currency currency)
+ {
+ Guard.NotNull(currency, nameof(currency));
+
+ if (currency.RoundOrderItemsEnabled)
+ {
+ return Math.Round(value, currency.RoundNumDecimals);
+ }
+
+ return value;
+ }
+
+ ///
+ /// Rounds and formats a decimal culture invariant
+ ///
+ /// Value to round
+ /// Rounding decimal number
+ /// Rounded and formated value
+ public static string FormatInvariant(this decimal value, int decimals = 2)
+ {
+ return Math.Round(value, decimals).ToString("0.00", CultureInfo.InvariantCulture);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs
index 4301d61e64..70bbe2eb28 100644
--- a/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/RouteExtensions.cs
@@ -14,6 +14,7 @@ public static string GetAreaName(this RouteData routeData)
{
return (area as string);
}
+
return routeData.Route.GetAreaName();
}
@@ -24,11 +25,13 @@ public static string GetAreaName(this RouteBase route)
{
return area.Area;
}
+
var route2 = route as Route;
if ((route2 != null) && (route2.DataTokens != null))
{
return (route2.DataTokens["area"] as string);
}
+
return null;
}
diff --git a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs
index f435df42ac..8d004331ed 100644
--- a/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs
+++ b/src/Libraries/SmartStore.Core/Extensions/StringExtensions.cs
@@ -740,18 +740,27 @@ public static string[] SplitSafe(this string value, string separator)
/// true: success, false: failure
[DebuggerStepThrough]
[SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")]
- public static bool SplitToPair(this string value, out string strLeft, out string strRight, string delimiter)
+ public static bool SplitToPair(this string value, out string leftPart, out string rightPart, string delimiter, bool splitAfterLast = false)
{
- int idx = -1;
- if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(delimiter) || (idx = value.IndexOf(delimiter)) == -1)
+ leftPart = value;
+ rightPart = "";
+
+ if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(delimiter))
+ {
+ return false;
+ }
+
+ var idx = splitAfterLast
+ ? value.LastIndexOf(delimiter)
+ : value.IndexOf(delimiter);
+
+ if (idx == -1)
{
- strLeft = value;
- strRight = "";
return false;
}
- strLeft = value.Substring(0, idx);
- strRight = value.Substring(idx + delimiter.Length);
+ leftPart = value.Substring(0, idx);
+ rightPart = value.Substring(idx + delimiter.Length);
return true;
}
@@ -883,7 +892,9 @@ public static string ToAttribute(this string value, string name, bool htmlEncode
return string.Format(" {0}=\"{1}\"", name, htmlEncode ? HttpUtility.HtmlEncode(value) : value);
}
- /// Appends grow and uses delimiter if the string is not empty.
+ ///
+ /// Appends grow and uses delimiter if the string is not empty.
+ ///
[DebuggerStepThrough]
public static string Grow(this string value, string grow, string delimiter)
{
@@ -893,7 +904,7 @@ public static string Grow(this string value, string grow, string delimiter)
if (string.IsNullOrEmpty(grow))
return (string.IsNullOrEmpty(value) ? "" : value);
- return string.Format("{0}{1}{2}", value, delimiter, grow);
+ return string.Concat(value, delimiter, grow);
}
/// Returns n/a if string is empty else self.
@@ -939,6 +950,29 @@ public static string Replace(this string value, string oldValue, string newValue
return value;
}
+ ///
+ /// Replaces digits in a string with culture native digits (if digit substitution for culture is required)
+ ///
+ [DebuggerStepThrough]
+ public static string ReplaceNativeDigits(this string value, IFormatProvider provider = null)
+ {
+ Guard.NotNull(value, nameof(value));
+
+ provider = provider ?? NumberFormatInfo.CurrentInfo;
+ var nfi = NumberFormatInfo.GetInstance(provider);
+
+ if (nfi.DigitSubstitution == DigitShapes.None)
+ {
+ return value;
+ }
+
+ var nativeDigits = nfi.NativeDigits;
+ var rg = new Regex(@"\d");
+
+ var result = rg.Replace(value, m => nativeDigits[m.Value.ToInt()]);
+ return result;
+ }
+
[DebuggerStepThrough]
public static string TrimSafe(this string value)
{
diff --git a/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs b/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs
index 98e482d393..3e645dcc1f 100644
--- a/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs
+++ b/src/Libraries/SmartStore.Core/Html/HtmlUtils.cs
@@ -294,7 +294,7 @@ public static string RelativizeFontSizes(string html, int baseFontSizePx = 16)
{
if (node.Style.FontSize is string s && s.EndsWith("px"))
{
- var size = s.Substring(0, s.Length - 2).Convert();
+ var size = s.Substring(0, s.Length - 2).Convert();
if (size > 0)
{
//node.Style.FontSize = Math.Round(((double)size / (double)baseFontSizePx), 4) + "em";
diff --git a/src/Libraries/SmartStore.Core/IO/IFile.cs b/src/Libraries/SmartStore.Core/IO/IFile.cs
index 0620feadd8..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,14 +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 2511c117c2..c90b886ffb 100644
--- a/src/Libraries/SmartStore.Core/IO/IFileSystem.cs
+++ b/src/Libraries/SmartStore.Core/IO/IFileSystem.cs
@@ -21,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.
@@ -69,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 2e8a08d6a6..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;
@@ -137,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);
}
@@ -201,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));
}
@@ -467,6 +482,7 @@ private class LocalFile : IFile
{
private readonly string _path;
private readonly FileInfo _fileInfo;
+ private Size? _dimensions;
public LocalFile(string path, FileInfo fileInfo)
{
@@ -479,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; }
@@ -494,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/IO/LockFile/LockFile.cs b/src/Libraries/SmartStore.Core/IO/LockFile/LockFile.cs
index c5086b0a63..1693635837 100644
--- a/src/Libraries/SmartStore.Core/IO/LockFile/LockFile.cs
+++ b/src/Libraries/SmartStore.Core/IO/LockFile/LockFile.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading;
+using System.IO;
using SmartStore.Utilities.Threading;
namespace SmartStore.Core.IO
@@ -32,9 +33,10 @@ public void Release()
{
using (_rwLock.GetWriteLock())
{
- if (_released || !_folder.FileExists(_path))
+ if (_released || !File.Exists(_folder.MapPath(_path)))
{
// nothing to do, might happen if re-granted or already released
+ // INFO: VirtualPathProvider caches file existence info, so not very reliable here.
return;
}
diff --git a/src/Libraries/SmartStore.Core/IO/LockFile/LockFileManager.cs b/src/Libraries/SmartStore.Core/IO/LockFile/LockFileManager.cs
index b5c7d5118d..ab48a467b8 100644
--- a/src/Libraries/SmartStore.Core/IO/LockFile/LockFileManager.cs
+++ b/src/Libraries/SmartStore.Core/IO/LockFile/LockFileManager.cs
@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.Threading;
+using System.IO;
using SmartStore.Utilities.Threading;
namespace SmartStore.Core.IO
@@ -74,7 +75,8 @@ public bool IsLocked(string path)
private bool IsLockedInternal(string path)
{
- if (_env.TenantFolder.FileExists(path))
+ // INFO: VirtualPathProvider caches file existence info, so not very reliable here.
+ if (File.Exists(_env.TenantFolder.MapPath(path)))
{
var content = _env.TenantFolder.ReadFile(path);
diff --git a/src/Libraries/SmartStore.Core/IO/MimeTypes.cs b/src/Libraries/SmartStore.Core/IO/MimeTypes.cs
index d9edddeef5..506821b06f 100644
--- a/src/Libraries/SmartStore.Core/IO/MimeTypes.cs
+++ b/src/Libraries/SmartStore.Core/IO/MimeTypes.cs
@@ -1,19 +1,767 @@
using System;
-using System.Collections.Concurrent;
+using System.Linq;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Web;
using Microsoft.Win32;
+using System.IO;
namespace SmartStore.Core.IO
{
public static class MimeTypes
{
- private static readonly ConcurrentDictionary _mimeMap = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+ private static readonly char[] _pathSeparatorChars = new [] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, Path.VolumeSeparatorChar };
+ private static readonly Dictionary _mimeMap;
+ private static readonly Dictionary _extMap;
- public static string MapNameToMimeType(string fileNameOrExtension)
+ const string DefaultMimeType = "application/octet-stream";
+
+ static MimeTypes()
+ {
+ // maps both ways,
+ // extension -> mime type
+ // and
+ // mime type -> extension
+ //
+ // any mime type on left side not pre-loaded on right side, are added automatically.
+ // Some mime types can map to multiple extensions, so to get a deterministic mapping,
+ // add those to the dictionary specifcially
+ //
+ // Some added based on http://www.iana.org/assignments/media-types/media-types.xhtml
+ // which lists mime types, but not extensions
+ //
+
+ _mimeMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ #region Mime types > extension map
+
+ {"application/fsharp-script", ".fsx"},
+ {"application/msaccess", ".adp"},
+ {"application/msword", ".doc"},
+ {"application/octet-stream", ".bin"},
+ {"application/onenote", ".one"},
+ {"application/postscript", ".eps"},
+ {"application/step", ".step"},
+ {"application/vnd.ms-excel", ".xls"},
+ {"application/vnd.ms-powerpoint", ".ppt"},
+ {"application/vnd.ms-works", ".wks"},
+ {"application/vnd.visio", ".vsd"},
+ {"application/x-director", ".dir"},
+ {"application/x-shockwave-flash", ".swf"},
+ {"application/x-x509-ca-cert", ".cer"},
+ {"application/x-zip-compressed", ".zip"},
+ {"application/xhtml+xml", ".xhtml"},
+ {"application/xml", ".xml"}, // anomoly, .xml -> text/xml, but application/xml -> many thingss, but all are xml, so safest is .xml
+ {"audio/aac", ".AAC"},
+ {"audio/aiff", ".aiff"},
+ {"audio/basic", ".snd"},
+ {"audio/mid", ".midi"},
+ {"audio/wav", ".wav"},
+ {"audio/x-m4a", ".m4a"},
+ {"audio/x-mpegurl", ".m3u"},
+ {"audio/x-pn-realaudio", ".ra"},
+ {"audio/x-smd", ".smd"},
+ {"image/bmp", ".bmp"},
+ {"image/jpeg", ".jpg"},
+ {"image/pict", ".pic"},
+ {"image/png", ".png"}, //Defined in [RFC-2045], [RFC-2048]
+ {"image/x-png", ".png"}, //See https://www.w3.org/TR/PNG/#A-Media-type :"It is recommended that implementations also recognize the media type "image/x-png"."
+ {"image/tiff", ".tiff"},
+ {"image/x-macpaint", ".mac"},
+ {"image/x-quicktime", ".qti"},
+ {"message/rfc822", ".eml"},
+ {"text/html", ".html"},
+ {"text/plain", ".txt"},
+ {"text/scriptlet", ".wsc"},
+ {"text/xml", ".xml"},
+ {"video/3gpp", ".3gp"},
+ {"video/3gpp2", ".3gp2"},
+ {"video/mp4", ".mp4"},
+ {"video/mpeg", ".mpg"},
+ {"video/quicktime", ".mov"},
+ {"video/vnd.dlna.mpeg-tts", ".m2t"},
+ {"video/x-dv", ".dv"},
+ {"video/x-la-asf", ".lsf"},
+ {"video/x-ms-asf", ".asf"},
+ {"x-world/x-vrml", ".xof"},
+
+ #endregion
+ };
+
+ _extMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ #region Extension > mime type map
+
+ {".323", "text/h323"},
+ {".3g2", "video/3gpp2"},
+ {".3gp", "video/3gpp"},
+ {".3gp2", "video/3gpp2"},
+ {".3gpp", "video/3gpp"},
+ {".7z", "application/x-7z-compressed"},
+ {".aa", "audio/audible"},
+ {".AAC", "audio/aac"},
+ {".aaf", "application/octet-stream"},
+ {".aax", "audio/vnd.audible.aax"},
+ {".ac3", "audio/ac3"},
+ {".aca", "application/octet-stream"},
+ {".accda", "application/msaccess.addin"},
+ {".accdb", "application/msaccess"},
+ {".accdc", "application/msaccess.cab"},
+ {".accde", "application/msaccess"},
+ {".accdr", "application/msaccess.runtime"},
+ {".accdt", "application/msaccess"},
+ {".accdw", "application/msaccess.webapplication"},
+ {".accft", "application/msaccess.ftemplate"},
+ {".acx", "application/internet-property-stream"},
+ {".AddIn", "text/xml"},
+ {".ade", "application/msaccess"},
+ {".adobebridge", "application/x-bridge-url"},
+ {".adp", "application/msaccess"},
+ {".ADT", "audio/vnd.dlna.adts"},
+ {".ADTS", "audio/aac"},
+ {".afm", "application/octet-stream"},
+ {".ai", "application/postscript"},
+ {".aif", "audio/aiff"},
+ {".aifc", "audio/aiff"},
+ {".aiff", "audio/aiff"},
+ {".air", "application/vnd.adobe.air-application-installer-package+zip"},
+ {".amc", "application/mpeg"},
+ {".anx", "application/annodex"},
+ {".apk", "application/vnd.android.package-archive" },
+ {".application", "application/x-ms-application"},
+ {".art", "image/x-jg"},
+ {".asa", "application/xml"},
+ {".asax", "application/xml"},
+ {".ascx", "application/xml"},
+ {".asd", "application/octet-stream"},
+ {".asf", "video/x-ms-asf"},
+ {".ashx", "application/xml"},
+ {".asi", "application/octet-stream"},
+ {".asm", "text/plain"},
+ {".asmx", "application/xml"},
+ {".aspx", "application/xml"},
+ {".asr", "video/x-ms-asf"},
+ {".asx", "video/x-ms-asf"},
+ {".atom", "application/atom+xml"},
+ {".au", "audio/basic"},
+ {".avi", "video/x-msvideo"},
+ {".axa", "audio/annodex"},
+ {".axs", "application/olescript"},
+ {".axv", "video/annodex"},
+ {".bas", "text/plain"},
+ {".bcpio", "application/x-bcpio"},
+ {".bin", "application/octet-stream"},
+ {".bmp", "image/bmp"},
+ {".c", "text/plain"},
+ {".cab", "application/octet-stream"},
+ {".caf", "audio/x-caf"},
+ {".calx", "application/vnd.ms-office.calx"},
+ {".cat", "application/vnd.ms-pki.seccat"},
+ {".cc", "text/plain"},
+ {".cd", "text/plain"},
+ {".cdda", "audio/aiff"},
+ {".cdf", "application/x-cdf"},
+ {".cer", "application/x-x509-ca-cert"},
+ {".cfg", "text/plain"},
+ {".chm", "application/octet-stream"},
+ {".class", "application/x-java-applet"},
+ {".clp", "application/x-msclip"},
+ {".cmd", "text/plain"},
+ {".cmx", "image/x-cmx"},
+ {".cnf", "text/plain"},
+ {".cod", "image/cis-cod"},
+ {".config", "application/xml"},
+ {".contact", "text/x-ms-contact"},
+ {".coverage", "application/xml"},
+ {".cpio", "application/x-cpio"},
+ {".cpp", "text/plain"},
+ {".crd", "application/x-mscardfile"},
+ {".crl", "application/pkix-crl"},
+ {".crt", "application/x-x509-ca-cert"},
+ {".cs", "text/plain"},
+ {".csdproj", "text/plain"},
+ {".csh", "application/x-csh"},
+ {".csproj", "text/plain"},
+ {".css", "text/css"},
+ {".csv", "text/csv"},
+ {".cur", "application/octet-stream"},
+ {".cxx", "text/plain"},
+ {".dat", "application/octet-stream"},
+ {".datasource", "application/xml"},
+ {".dbproj", "text/plain"},
+ {".dcr", "application/x-director"},
+ {".def", "text/plain"},
+ {".deploy", "application/octet-stream"},
+ {".der", "application/x-x509-ca-cert"},
+ {".dgml", "application/xml"},
+ {".dib", "image/bmp"},
+ {".dif", "video/x-dv"},
+ {".dir", "application/x-director"},
+ {".disco", "text/xml"},
+ {".divx", "video/divx"},
+ {".dll", "application/x-msdownload"},
+ {".dll.config", "text/xml"},
+ {".dlm", "text/dlm"},
+ {".doc", "application/msword"},
+ {".docm", "application/vnd.ms-word.document.macroEnabled.12"},
+ {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
+ {".dot", "application/msword"},
+ {".dotm", "application/vnd.ms-word.template.macroEnabled.12"},
+ {".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"},
+ {".dsp", "application/octet-stream"},
+ {".dsw", "text/plain"},
+ {".dtd", "text/xml"},
+ {".dtsConfig", "text/xml"},
+ {".dv", "video/x-dv"},
+ {".dvi", "application/x-dvi"},
+ {".dwf", "drawing/x-dwf"},
+ {".dwg", "application/acad"},
+ {".dwp", "application/octet-stream"},
+ {".dxf", "application/x-dxf" },
+ {".dxr", "application/x-director"},
+ {".eml", "message/rfc822"},
+ {".emz", "application/octet-stream"},
+ {".eot", "application/vnd.ms-fontobject"},
+ {".eps", "application/postscript"},
+ {".etl", "application/etl"},
+ {".etx", "text/x-setext"},
+ {".evy", "application/envoy"},
+ {".exe", "application/octet-stream"},
+ {".exe.config", "text/xml"},
+ {".fdf", "application/vnd.fdf"},
+ {".fif", "application/fractals"},
+ {".filters", "application/xml"},
+ {".fla", "application/octet-stream"},
+ {".flac", "audio/flac"},
+ {".flr", "x-world/x-vrml"},
+ {".flv", "video/x-flv"},
+ {".fsscript", "application/fsharp-script"},
+ {".fsx", "application/fsharp-script"},
+ {".generictest", "application/xml"},
+ {".gif", "image/gif"},
+ {".gpx", "application/gpx+xml"},
+ {".group", "text/x-ms-group"},
+ {".gsm", "audio/x-gsm"},
+ {".gtar", "application/x-gtar"},
+ {".gz", "application/x-gzip"},
+ {".h", "text/plain"},
+ {".hdf", "application/x-hdf"},
+ {".hdml", "text/x-hdml"},
+ {".hhc", "application/x-oleobject"},
+ {".hhk", "application/octet-stream"},
+ {".hhp", "application/octet-stream"},
+ {".hlp", "application/winhlp"},
+ {".hpp", "text/plain"},
+ {".hqx", "application/mac-binhex40"},
+ {".hta", "application/hta"},
+ {".htc", "text/x-component"},
+ {".htm", "text/html"},
+ {".html", "text/html"},
+ {".htt", "text/webviewhtml"},
+ {".hxa", "application/xml"},
+ {".hxc", "application/xml"},
+ {".hxd", "application/octet-stream"},
+ {".hxe", "application/xml"},
+ {".hxf", "application/xml"},
+ {".hxh", "application/octet-stream"},
+ {".hxi", "application/octet-stream"},
+ {".hxk", "application/xml"},
+ {".hxq", "application/octet-stream"},
+ {".hxr", "application/octet-stream"},
+ {".hxs", "application/octet-stream"},
+ {".hxt", "text/html"},
+ {".hxv", "application/xml"},
+ {".hxw", "application/octet-stream"},
+ {".hxx", "text/plain"},
+ {".i", "text/plain"},
+ {".ico", "image/x-icon"},
+ {".ics", "application/octet-stream"},
+ {".idl", "text/plain"},
+ {".ief", "image/ief"},
+ {".iii", "application/x-iphone"},
+ {".inc", "text/plain"},
+ {".inf", "application/octet-stream"},
+ {".ini", "text/plain"},
+ {".inl", "text/plain"},
+ {".ins", "application/x-internet-signup"},
+ {".ipa", "application/x-itunes-ipa"},
+ {".ipg", "application/x-itunes-ipg"},
+ {".ipproj", "text/plain"},
+ {".ipsw", "application/x-itunes-ipsw"},
+ {".iqy", "text/x-ms-iqy"},
+ {".isp", "application/x-internet-signup"},
+ {".ite", "application/x-itunes-ite"},
+ {".itlp", "application/x-itunes-itlp"},
+ {".itms", "application/x-itunes-itms"},
+ {".itpc", "application/x-itunes-itpc"},
+ {".IVF", "video/x-ivf"},
+ {".jar", "application/java-archive"},
+ {".java", "application/octet-stream"},
+ {".jck", "application/liquidmotion"},
+ {".jcz", "application/liquidmotion"},
+ {".jfif", "image/pjpeg"},
+ {".jnlp", "application/x-java-jnlp-file"},
+ {".jpb", "application/octet-stream"},
+ {".jpe", "image/jpeg"},
+ {".jpeg", "image/jpeg"},
+ {".jpg", "image/jpeg"},
+ {".js", "application/javascript"},
+ {".json", "application/json"},
+ {".jsx", "text/jscript"},
+ {".jsxbin", "text/plain"},
+ {".latex", "application/x-latex"},
+ {".library-ms", "application/windows-library+xml"},
+ {".lit", "application/x-ms-reader"},
+ {".loadtest", "application/xml"},
+ {".lpk", "application/octet-stream"},
+ {".lsf", "video/x-la-asf"},
+ {".lst", "text/plain"},
+ {".lsx", "video/x-la-asf"},
+ {".lzh", "application/octet-stream"},
+ {".m13", "application/x-msmediaview"},
+ {".m14", "application/x-msmediaview"},
+ {".m1v", "video/mpeg"},
+ {".m2t", "video/vnd.dlna.mpeg-tts"},
+ {".m2ts", "video/vnd.dlna.mpeg-tts"},
+ {".m2v", "video/mpeg"},
+ {".m3u", "audio/x-mpegurl"},
+ {".m3u8", "audio/x-mpegurl"},
+ {".m4a", "audio/m4a"},
+ {".m4b", "audio/m4b"},
+ {".m4p", "audio/m4p"},
+ {".m4r", "audio/x-m4r"},
+ {".m4v", "video/x-m4v"},
+ {".mac", "image/x-macpaint"},
+ {".mak", "text/plain"},
+ {".man", "application/x-troff-man"},
+ {".manifest", "application/x-ms-manifest"},
+ {".map", "text/plain"},
+ {".master", "application/xml"},
+ {".mbox", "application/mbox"},
+ {".mda", "application/msaccess"},
+ {".mdb", "application/x-msaccess"},
+ {".mde", "application/msaccess"},
+ {".mdp", "application/octet-stream"},
+ {".me", "application/x-troff-me"},
+ {".mfp", "application/x-shockwave-flash"},
+ {".mht", "message/rfc822"},
+ {".mhtml", "message/rfc822"},
+ {".mid", "audio/mid"},
+ {".midi", "audio/mid"},
+ {".mix", "application/octet-stream"},
+ {".mk", "text/plain"},
+ {".mk3d", "video/x-matroska-3d"},
+ {".mka", "audio/x-matroska"},
+ {".mkv", "video/x-matroska"},
+ {".mmf", "application/x-smaf"},
+ {".mno", "text/xml"},
+ {".mny", "application/x-msmoney"},
+ {".mod", "video/mpeg"},
+ {".mov", "video/quicktime"},
+ {".movie", "video/x-sgi-movie"},
+ {".mp2", "video/mpeg"},
+ {".mp2v", "video/mpeg"},
+ {".mp3", "audio/mpeg"},
+ {".mp4", "video/mp4"},
+ {".mp4v", "video/mp4"},
+ {".mpa", "video/mpeg"},
+ {".mpe", "video/mpeg"},
+ {".mpeg", "video/mpeg"},
+ {".mpf", "application/vnd.ms-mediapackage"},
+ {".mpg", "video/mpeg"},
+ {".mpp", "application/vnd.ms-project"},
+ {".mpv2", "video/mpeg"},
+ {".mqv", "video/quicktime"},
+ {".ms", "application/x-troff-ms"},
+ {".msg", "application/vnd.ms-outlook"},
+ {".msi", "application/octet-stream"},
+ {".mso", "application/octet-stream"},
+ {".mts", "video/vnd.dlna.mpeg-tts"},
+ {".mtx", "application/xml"},
+ {".mvb", "application/x-msmediaview"},
+ {".mvc", "application/x-miva-compiled"},
+ {".mxp", "application/x-mmxp"},
+ {".nc", "application/x-netcdf"},
+ {".nsc", "video/x-ms-asf"},
+ {".nws", "message/rfc822"},
+ {".ocx", "application/octet-stream"},
+ {".oda", "application/oda"},
+ {".odb", "application/vnd.oasis.opendocument.database"},
+ {".odc", "application/vnd.oasis.opendocument.chart"},
+ {".odf", "application/vnd.oasis.opendocument.formula"},
+ {".odg", "application/vnd.oasis.opendocument.graphics"},
+ {".odh", "text/plain"},
+ {".odi", "application/vnd.oasis.opendocument.image"},
+ {".odl", "text/plain"},
+ {".odm", "application/vnd.oasis.opendocument.text-master"},
+ {".odp", "application/vnd.oasis.opendocument.presentation"},
+ {".ods", "application/vnd.oasis.opendocument.spreadsheet"},
+ {".odt", "application/vnd.oasis.opendocument.text"},
+ {".oga", "audio/ogg"},
+ {".ogg", "audio/ogg"},
+ {".ogv", "video/ogg"},
+ {".ogx", "application/ogg"},
+ {".one", "application/onenote"},
+ {".onea", "application/onenote"},
+ {".onepkg", "application/onenote"},
+ {".onetmp", "application/onenote"},
+ {".onetoc", "application/onenote"},
+ {".onetoc2", "application/onenote"},
+ {".opus", "audio/ogg"},
+ {".orderedtest", "application/xml"},
+ {".osdx", "application/opensearchdescription+xml"},
+ {".otf", "application/font-sfnt"},
+ {".otg", "application/vnd.oasis.opendocument.graphics-template"},
+ {".oth", "application/vnd.oasis.opendocument.text-web"},
+ {".otp", "application/vnd.oasis.opendocument.presentation-template"},
+ {".ots", "application/vnd.oasis.opendocument.spreadsheet-template"},
+ {".ott", "application/vnd.oasis.opendocument.text-template"},
+ {".oxt", "application/vnd.openofficeorg.extension"},
+ {".p10", "application/pkcs10"},
+ {".p12", "application/x-pkcs12"},
+ {".p7b", "application/x-pkcs7-certificates"},
+ {".p7c", "application/pkcs7-mime"},
+ {".p7m", "application/pkcs7-mime"},
+ {".p7r", "application/x-pkcs7-certreqresp"},
+ {".p7s", "application/pkcs7-signature"},
+ {".pbm", "image/x-portable-bitmap"},
+ {".pcast", "application/x-podcast"},
+ {".pct", "image/pict"},
+ {".pcx", "application/octet-stream"},
+ {".pcz", "application/octet-stream"},
+ {".pdf", "application/pdf"},
+ {".pfb", "application/octet-stream"},
+ {".pfm", "application/octet-stream"},
+ {".pfx", "application/x-pkcs12"},
+ {".pgm", "image/x-portable-graymap"},
+ {".pic", "image/pict"},
+ {".pict", "image/pict"},
+ {".pkgdef", "text/plain"},
+ {".pkgundef", "text/plain"},
+ {".pko", "application/vnd.ms-pki.pko"},
+ {".pls", "audio/scpls"},
+ {".pma", "application/x-perfmon"},
+ {".pmc", "application/x-perfmon"},
+ {".pml", "application/x-perfmon"},
+ {".pmr", "application/x-perfmon"},
+ {".pmw", "application/x-perfmon"},
+ {".png", "image/png"},
+ {".pnm", "image/x-portable-anymap"},
+ {".pnt", "image/x-macpaint"},
+ {".pntg", "image/x-macpaint"},
+ {".pnz", "image/png"},
+ {".pot", "application/vnd.ms-powerpoint"},
+ {".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"},
+ {".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"},
+ {".ppa", "application/vnd.ms-powerpoint"},
+ {".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"},
+ {".ppm", "image/x-portable-pixmap"},
+ {".pps", "application/vnd.ms-powerpoint"},
+ {".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"},
+ {".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"},
+ {".ppt", "application/vnd.ms-powerpoint"},
+ {".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"},
+ {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
+ {".prf", "application/pics-rules"},
+ {".prm", "application/octet-stream"},
+ {".prx", "application/octet-stream"},
+ {".ps", "application/postscript"},
+ {".psc1", "application/PowerShell"},
+ {".psd", "application/octet-stream"},
+ {".psess", "application/xml"},
+ {".psm", "application/octet-stream"},
+ {".psp", "application/octet-stream"},
+ {".pst", "application/vnd.ms-outlook"},
+ {".pub", "application/x-mspublisher"},
+ {".pwz", "application/vnd.ms-powerpoint"},
+ {".qht", "text/x-html-insertion"},
+ {".qhtm", "text/x-html-insertion"},
+ {".qt", "video/quicktime"},
+ {".qti", "image/x-quicktime"},
+ {".qtif", "image/x-quicktime"},
+ {".qtl", "application/x-quicktimeplayer"},
+ {".qxd", "application/octet-stream"},
+ {".ra", "audio/x-pn-realaudio"},
+ {".ram", "audio/x-pn-realaudio"},
+ {".rar", "application/x-rar-compressed"},
+ {".ras", "image/x-cmu-raster"},
+ {".rat", "application/rat-file"},
+ {".rc", "text/plain"},
+ {".rc2", "text/plain"},
+ {".rct", "text/plain"},
+ {".rdlc", "application/xml"},
+ {".reg", "text/plain"},
+ {".resx", "application/xml"},
+ {".rf", "image/vnd.rn-realflash"},
+ {".rgb", "image/x-rgb"},
+ {".rgs", "text/plain"},
+ {".rm", "application/vnd.rn-realmedia"},
+ {".rmi", "audio/mid"},
+ {".rmp", "application/vnd.rn-rn_music_package"},
+ {".roff", "application/x-troff"},
+ {".rpm", "audio/x-pn-realaudio-plugin"},
+ {".rqy", "text/x-ms-rqy"},
+ {".rtf", "application/rtf"},
+ {".rtx", "text/richtext"},
+ {".rvt", "application/octet-stream" },
+ {".ruleset", "application/xml"},
+ {".s", "text/plain"},
+ {".safariextz", "application/x-safari-safariextz"},
+ {".scd", "application/x-msschedule"},
+ {".scr", "text/plain"},
+ {".sct", "text/scriptlet"},
+ {".sd2", "audio/x-sd2"},
+ {".sdp", "application/sdp"},
+ {".sea", "application/octet-stream"},
+ {".searchConnector-ms", "application/windows-search-connector+xml"},
+ {".setpay", "application/set-payment-initiation"},
+ {".setreg", "application/set-registration-initiation"},
+ {".settings", "application/xml"},
+ {".sgimb", "application/x-sgimb"},
+ {".sgml", "text/sgml"},
+ {".sh", "application/x-sh"},
+ {".shar", "application/x-shar"},
+ {".shtml", "text/html"},
+ {".sit", "application/x-stuffit"},
+ {".sitemap", "application/xml"},
+ {".skin", "application/xml"},
+ {".skp", "application/x-koan" },
+ {".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"},
+ {".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"},
+ {".slk", "application/vnd.ms-excel"},
+ {".sln", "text/plain"},
+ {".slupkg-ms", "application/x-ms-license"},
+ {".smd", "audio/x-smd"},
+ {".smi", "application/octet-stream"},
+ {".smx", "audio/x-smd"},
+ {".smz", "audio/x-smd"},
+ {".snd", "audio/basic"},
+ {".snippet", "application/xml"},
+ {".snp", "application/octet-stream"},
+ {".sol", "text/plain"},
+ {".sor", "text/plain"},
+ {".spc", "application/x-pkcs7-certificates"},
+ {".spl", "application/futuresplash"},
+ {".spx", "audio/ogg"},
+ {".src", "application/x-wais-source"},
+ {".srf", "text/plain"},
+ {".SSISDeploymentManifest", "text/xml"},
+ {".ssm", "application/streamingmedia"},
+ {".sst", "application/vnd.ms-pki.certstore"},
+ {".stl", "application/vnd.ms-pki.stl"},
+ {".sv4cpio", "application/x-sv4cpio"},
+ {".sv4crc", "application/x-sv4crc"},
+ {".svc", "application/xml"},
+ {".svg", "image/svg+xml"},
+ {".swf", "application/x-shockwave-flash"},
+ {".step", "application/step"},
+ {".stp", "application/step"},
+ {".t", "application/x-troff"},
+ {".tar", "application/x-tar"},
+ {".tcl", "application/x-tcl"},
+ {".testrunconfig", "application/xml"},
+ {".testsettings", "application/xml"},
+ {".tex", "application/x-tex"},
+ {".texi", "application/x-texinfo"},
+ {".texinfo", "application/x-texinfo"},
+ {".tgz", "application/x-compressed"},
+ {".thmx", "application/vnd.ms-officetheme"},
+ {".thn", "application/octet-stream"},
+ {".tif", "image/tiff"},
+ {".tiff", "image/tiff"},
+ {".tlh", "text/plain"},
+ {".tli", "text/plain"},
+ {".toc", "application/octet-stream"},
+ {".tr", "application/x-troff"},
+ {".trm", "application/x-msterminal"},
+ {".trx", "application/xml"},
+ {".ts", "video/vnd.dlna.mpeg-tts"},
+ {".tsv", "text/tab-separated-values"},
+ {".ttf", "application/font-sfnt"},
+ {".tts", "video/vnd.dlna.mpeg-tts"},
+ {".txt", "text/plain"},
+ {".u32", "application/octet-stream"},
+ {".uls", "text/iuls"},
+ {".user", "text/plain"},
+ {".ustar", "application/x-ustar"},
+ {".vb", "text/plain"},
+ {".vbdproj", "text/plain"},
+ {".vbk", "video/mpeg"},
+ {".vbproj", "text/plain"},
+ {".vbs", "text/vbscript"},
+ {".vcf", "text/x-vcard"},
+ {".vcproj", "application/xml"},
+ {".vcs", "text/plain"},
+ {".vcxproj", "application/xml"},
+ {".vddproj", "text/plain"},
+ {".vdp", "text/plain"},
+ {".vdproj", "text/plain"},
+ {".vdx", "application/vnd.ms-visio.viewer"},
+ {".vml", "text/xml"},
+ {".vscontent", "application/xml"},
+ {".vsct", "text/xml"},
+ {".vsd", "application/vnd.visio"},
+ {".vsi", "application/ms-vsi"},
+ {".vsix", "application/vsix"},
+ {".vsixlangpack", "text/xml"},
+ {".vsixmanifest", "text/xml"},
+ {".vsmdi", "application/xml"},
+ {".vspscc", "text/plain"},
+ {".vss", "application/vnd.visio"},
+ {".vsscc", "text/plain"},
+ {".vssettings", "text/xml"},
+ {".vssscc", "text/plain"},
+ {".vst", "application/vnd.visio"},
+ {".vstemplate", "text/xml"},
+ {".vsto", "application/x-ms-vsto"},
+ {".vsw", "application/vnd.visio"},
+ {".vsx", "application/vnd.visio"},
+ {".vtt", "text/vtt"},
+ {".vtx", "application/vnd.visio"},
+ {".wasm", "application/wasm"},
+ {".wav", "audio/wav"},
+ {".wave", "audio/wav"},
+ {".wax", "audio/x-ms-wax"},
+ {".wbk", "application/msword"},
+ {".wbmp", "image/vnd.wap.wbmp"},
+ {".wcm", "application/vnd.ms-works"},
+ {".wdb", "application/vnd.ms-works"},
+ {".wdp", "image/vnd.ms-photo"},
+ {".webarchive", "application/x-safari-webarchive"},
+ {".webm", "video/webm"},
+ {".webp", "image/webp"}, /* https://en.wikipedia.org/wiki/WebP */
+ {".webtest", "application/xml"},
+ {".wiq", "application/xml"},
+ {".wiz", "application/msword"},
+ {".wks", "application/vnd.ms-works"},
+ {".WLMP", "application/wlmoviemaker"},
+ {".wlpginstall", "application/x-wlpg-detect"},
+ {".wlpginstall3", "application/x-wlpg3-detect"},
+ {".wm", "video/x-ms-wm"},
+ {".wma", "audio/x-ms-wma"},
+ {".wmd", "application/x-ms-wmd"},
+ {".wmf", "application/x-msmetafile"},
+ {".wml", "text/vnd.wap.wml"},
+ {".wmlc", "application/vnd.wap.wmlc"},
+ {".wmls", "text/vnd.wap.wmlscript"},
+ {".wmlsc", "application/vnd.wap.wmlscriptc"},
+ {".wmp", "video/x-ms-wmp"},
+ {".wmv", "video/x-ms-wmv"},
+ {".wmx", "video/x-ms-wmx"},
+ {".wmz", "application/x-ms-wmz"},
+ {".woff", "application/font-woff"},
+ {".woff2", "application/font-woff2"},
+ {".wpl", "application/vnd.ms-wpl"},
+ {".wps", "application/vnd.ms-works"},
+ {".wri", "application/x-mswrite"},
+ {".wrl", "x-world/x-vrml"},
+ {".wrz", "x-world/x-vrml"},
+ {".wsc", "text/scriptlet"},
+ {".wsdl", "text/xml"},
+ {".wvx", "video/x-ms-wvx"},
+ {".x", "application/directx"},
+ {".xaf", "x-world/x-vrml"},
+ {".xaml", "application/xaml+xml"},
+ {".xap", "application/x-silverlight-app"},
+ {".xbap", "application/x-ms-xbap"},
+ {".xbm", "image/x-xbitmap"},
+ {".xdr", "text/plain"},
+ {".xht", "application/xhtml+xml"},
+ {".xhtml", "application/xhtml+xml"},
+ {".xla", "application/vnd.ms-excel"},
+ {".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"},
+ {".xlc", "application/vnd.ms-excel"},
+ {".xld", "application/vnd.ms-excel"},
+ {".xlk", "application/vnd.ms-excel"},
+ {".xll", "application/vnd.ms-excel"},
+ {".xlm", "application/vnd.ms-excel"},
+ {".xls", "application/vnd.ms-excel"},
+ {".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"},
+ {".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"},
+ {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
+ {".xlt", "application/vnd.ms-excel"},
+ {".xltm", "application/vnd.ms-excel.template.macroEnabled.12"},
+ {".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"},
+ {".xlw", "application/vnd.ms-excel"},
+ {".xml", "text/xml"},
+ {".xmp", "application/octet-stream" },
+ {".xmta", "application/xml"},
+ {".xof", "x-world/x-vrml"},
+ {".XOML", "text/plain"},
+ {".xpm", "image/x-xpixmap"},
+ {".xps", "application/vnd.ms-xpsdocument"},
+ {".xrm-ms", "text/xml"},
+ {".xsc", "application/xml"},
+ {".xsd", "text/xml"},
+ {".xsf", "text/xml"},
+ {".xsl", "text/xml"},
+ {".xslt", "text/xml"},
+ {".xsn", "application/octet-stream"},
+ {".xss", "application/xml"},
+ {".xspf", "application/xspf+xml"},
+ {".xtp", "application/octet-stream"},
+ {".xwd", "image/x-xwindowdump"},
+ {".z", "application/x-compress"},
+ {".zip", "application/zip"},
+
+ #endregion
+ };
+
+ // Shuffle entries
+ ShuffleMap(_mimeMap, _extMap);
+ ShuffleMap(_extMap, _mimeMap);
+
+ // Complement maps with entries from registry
+ try
+ {
+ using (var key = Registry.ClassesRoot.OpenSubKey(@"MIME\Database\Content Type", false))
+ {
+ if (key != null)
+ {
+ var subKeyNames = key.GetSubKeyNames();
+ foreach (var contentType in subKeyNames)
+ {
+ if (!_mimeMap.ContainsKey(contentType))
+ {
+ // Our map does not contain this entry. We gonna add it.
+ using (var subKey = key.OpenSubKey(contentType, false))
+ {
+ object extension = subKey.GetValue("Extension", null);
+ if (extension != null)
+ {
+ // MimeType > Extension
+ var ext = extension.ToString();
+ _mimeMap.Add(contentType, ext);
+
+ // Vice versa, Extension > MimeType
+ if (!_extMap.ContainsKey(ext))
+ {
+ _extMap.Add(ext, contentType);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ catch { }
+ }
+
+ public static string MapNameToMimeType(string fileNameOrExtension)
{
- return MimeMapping.GetMimeMapping(fileNameOrExtension);
+ var fileName = GetFileName(fileNameOrExtension);
+
+ for (int i = 0; i < fileName.Length; i++)
+ {
+ if ((fileName[i] == '.') && _extMap.TryGetValue(fileName.Substring(i), out var str))
+ {
+ return str;
+ }
+ }
+
+
+ return DefaultMimeType;
}
///
@@ -27,37 +775,66 @@ public static string MapMimeTypeToExtension(string mimeType)
if (mimeType.IsEmpty())
return null;
- return _mimeMap.GetOrAdd(mimeType, k => {
- string result;
+ if (_mimeMap.TryGetValue(mimeType, out var extension))
+ {
+ return extension.TrimStart('.');
+ }
- try
- {
- using (var key = Registry.ClassesRoot.OpenSubKey(@"MIME\Database\Content Type\" + mimeType, false))
- {
- object value = key != null ? key.GetValue("Extension", null) : null;
- result = value != null ? value.ToString().Trim('.') : null;
- }
- }
- catch
+ return null;
+
+ //return _mimeMap.GetOrAdd(mimeType, k => {
+ // string result;
+
+ // try
+ // {
+ // using (var key = Registry.ClassesRoot.OpenSubKey(@"MIME\Database\Content Type\" + mimeType, false))
+ // {
+ // object value = key != null ? key.GetValue("Extension", null) : null;
+ // result = value != null ? value.ToString().Trim('.') : null;
+ // }
+ // }
+ // catch
+ // {
+ // string[] parts = mimeType.Split('/');
+ // result = parts[parts.Length - 1];
+ // switch (result)
+ // {
+ // case "pjpeg":
+ // result = "jpg";
+ // break;
+ // case "x-png":
+ // result = "png";
+ // break;
+ // case "x-icon":
+ // result = "ico";
+ // break;
+ // }
+ // }
+
+ // return result;
+ //});
+ }
+
+ private static void ShuffleMap(Dictionary source, Dictionary target)
+ {
+ foreach (var kvp in source)
+ {
+ if (!target.ContainsKey(kvp.Value))
{
- string[] parts = mimeType.Split('/');
- result = parts[parts.Length - 1];
- switch (result)
- {
- case "pjpeg":
- result = "jpg";
- break;
- case "x-png":
- result = "png";
- break;
- case "x-icon":
- result = "ico";
- break;
- }
+ target.Add(kvp.Value, kvp.Key);
}
+ }
+ }
- return result;
- });
- }
- }
+ private static string GetFileName(string path)
+ {
+ var startIndex = path.LastIndexOfAny(_pathSeparatorChars);
+ if (startIndex < 0)
+ {
+ return path;
+ }
+
+ return path.Substring(startIndex);
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/IO/SymLink/FileSystemInfoExtensions.cs b/src/Libraries/SmartStore.Core/IO/SymLink/FileSystemInfoExtensions.cs
new file mode 100644
index 0000000000..2c7ed00c92
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/IO/SymLink/FileSystemInfoExtensions.cs
@@ -0,0 +1,29 @@
+using System;
+using System.IO;
+using SmartStore.Core.IO;
+
+namespace SmartStore
+{
+ public static class FileSystemInfoExtensions
+ {
+ ///
+ /// Determines whether this file system entry is a symbolic link.
+ ///
+ /// The directory or file in question.
+ /// true
if the entry is a symbolic link, false
otherwise.
+ public static bool IsSymbolicLink(this FileSystemInfo fsi)
+ {
+ return SymbolicLink.IsSymbolicLink(fsi);
+ }
+
+ ///
+ /// Returns the full path to the target of a symbolic link or mount.
+ ///
+ /// The symbolic link in question.
+ /// The path to the target.
+ public static string GetFinalPathName(this FileSystemInfo fsi)
+ {
+ return SymbolicLink.GetFinalPathName(fsi.FullName);
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/IO/SymLink/SymbolicLink.cs b/src/Libraries/SmartStore.Core/IO/SymLink/SymbolicLink.cs
new file mode 100644
index 0000000000..09567cf1eb
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/IO/SymLink/SymbolicLink.cs
@@ -0,0 +1,103 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace SmartStore.Core.IO
+{
+ internal static class SymbolicLink
+ {
+ [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
+ private static extern IntPtr CreateFile(
+ [MarshalAs(UnmanagedType.LPTStr)] string filename,
+ [MarshalAs(UnmanagedType.U4)] uint access,
+ [MarshalAs(UnmanagedType.U4)] FileShare share,
+ IntPtr securityAttributes, // optional SECURITY_ATTRIBUTES struct or IntPtr.Zero
+ [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
+ [MarshalAs(UnmanagedType.U4)] uint flagsAndAttributes,
+ IntPtr templateFile);
+
+ //[DllImport("kernel32.dll", EntryPoint = "CreateSymbolicLinkW", CharSet = CharSet.Unicode, SetLastError = true)]
+ //private static extern bool CreateSymbolicLink(
+ // [In] string lpSymlinkFileName,
+ // [In] string lpTargetFileName,
+ // [In] int dwFlags);
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
+ private static extern uint GetFinalPathNameByHandle(
+ IntPtr hFile,
+ [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszFilePath,
+ uint cchFilePath,
+ uint dwFlags);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ static extern bool CloseHandle(IntPtr hObject);
+
+ private const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
+ private const uint FILE_READ_EA = 0x0008;
+
+ private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
+
+ public static bool IsSymbolicLink(FileSystemInfo fsi)
+ {
+ Guard.NotNull(fsi, nameof(fsi));
+
+ if (!fsi.Exists)
+ return false;
+
+ if (fsi.Attributes.HasFlag(FileAttributes.ReparsePoint))
+ {
+ var target = GetFinalPathName(fsi.FullName);
+ if (target.HasValue())
+ {
+ return !string.Equals(target.TrimEnd('\\'), fsi.FullName.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ return false;
+ }
+
+ public static string GetFinalPathName(string path)
+ {
+ Guard.NotEmpty(path, nameof(path));
+
+ var h = CreateFile(path,
+ FILE_READ_EA,
+ FileShare.ReadWrite | FileShare.Delete,
+ IntPtr.Zero,
+ FileMode.Open,
+ FILE_FLAG_BACKUP_SEMANTICS,
+ IntPtr.Zero);
+
+ if (h == INVALID_HANDLE_VALUE)
+ {
+ throw new Win32Exception(Marshal.GetLastWin32Error());
+ }
+
+ try
+ {
+ var sb = new StringBuilder(1024);
+ var res = GetFinalPathNameByHandle(h, sb, 1024, 0);
+ if (res == 0)
+ {
+ throw new Win32Exception(Marshal.GetLastWin32Error());
+ }
+
+ var result = sb.ToString();
+
+ if (result.Length >= 4 && result.StartsWith(@"\\?\"))
+ {
+ return result.Substring(4);// remove "\\?\"
+ }
+
+ return result;
+ }
+ finally
+ {
+ CloseHandle(h);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Libraries/SmartStore.Core/IO/VirtualPath/DefaultVirtualPathProvider.cs b/src/Libraries/SmartStore.Core/IO/VirtualPath/DefaultVirtualPathProvider.cs
index c173ed8549..2d78ea0447 100644
--- a/src/Libraries/SmartStore.Core/IO/VirtualPath/DefaultVirtualPathProvider.cs
+++ b/src/Libraries/SmartStore.Core/IO/VirtualPath/DefaultVirtualPathProvider.cs
@@ -6,7 +6,6 @@
using System.Web.Caching;
using SmartStore.Utilities;
using System.Web;
-using SmartStore.Core.Infrastructure.DependencyManagement;
using SmartStore.Core.Logging;
namespace SmartStore.Core.IO
diff --git a/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs b/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs
index 7db7ae9f7c..8b892dedea 100644
--- a/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs
+++ b/src/Libraries/SmartStore.Core/IO/VirtualPath/VirtualFolder.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using SmartStore.Core.Infrastructure.DependencyManagement;
using SmartStore.Core.Logging;
namespace SmartStore.Core.IO
diff --git a/src/Libraries/SmartStore.Core/IPageable.cs b/src/Libraries/SmartStore.Core/IPageable.cs
index ef3ce500a2..6689becf09 100644
--- a/src/Libraries/SmartStore.Core/IPageable.cs
+++ b/src/Libraries/SmartStore.Core/IPageable.cs
@@ -82,10 +82,17 @@ public interface IPageable : IEnumerable
public interface IPagedList : IPageable, IList
{
///
- /// Return the original query without any paging applied
+ /// Gets underlying query without any paging applied
///
IQueryable SourceQuery { get; }
+ ///
+ /// Allows modification of the underlying query before it is executed.
+ ///
+ /// The alteration function. The underlying query is passed, the modified query should be returned.
+ /// The current instance for chaining
+ IPagedList AlterQuery(Func, IQueryable> alterer);
+
///
/// Applies the initial paging arguments to the passed query
///
diff --git a/src/Libraries/SmartStore.Core/Infrastructure/ComparableObject.cs b/src/Libraries/SmartStore.Core/Infrastructure/ComparableObject.cs
index 69a9040025..153405382b 100644
--- a/src/Libraries/SmartStore.Core/Infrastructure/ComparableObject.cs
+++ b/src/Libraries/SmartStore.Core/Infrastructure/ComparableObject.cs
@@ -175,7 +175,6 @@ protected void RegisterSignatureProperty(string propertyName)
[Serializable]
public abstract class ComparableObject : ComparableObject, IEquatable
{
-
///
/// Adds an extra property to the type specific signature properties list.
///
@@ -198,7 +197,6 @@ public virtual bool Equals(T other)
return base.Equals(other);
}
-
}
}
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/Money.cs b/src/Libraries/SmartStore.Core/Money.cs
new file mode 100644
index 0000000000..e4bdf4a24f
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Money.cs
@@ -0,0 +1,477 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Runtime.Serialization;
+using SmartStore.Core.Domain.Directory;
+
+namespace SmartStore
+{
+ public class Money : IConvertible, IFormattable, IComparable, IComparable, IEquatable
+ {
+ public Money(Currency currency)
+ : this(0m, currency)
+ {
+ }
+
+ public Money(float amount, Currency currency)
+ : this((decimal)amount, currency, false)
+ {
+ }
+
+ public Money(double amount, Currency currency)
+ : this((decimal)amount, currency, false)
+ {
+ }
+
+ public Money(decimal amount, Currency currency)
+ : this(amount, currency, false)
+ {
+ }
+
+ public Money(decimal amount, Currency currency, bool hideCurrency)
+ {
+ Guard.NotNull(currency, nameof(currency));
+
+ Amount = amount;
+ Currency = currency;
+ HideCurrency = hideCurrency;
+ }
+
+ [IgnoreDataMember]
+ public bool HideCurrency
+ {
+ get;
+ internal set;
+ }
+
+ [IgnoreDataMember]
+ public Currency Currency
+ {
+ get;
+ internal set;
+ }
+
+ ///
+ /// Gets the number of decimal digits for the associated currency.
+ ///
+ public int DecimalDigits
+ {
+ get => string.Equals(Currency?.CurrencyCode, "btc", StringComparison.OrdinalIgnoreCase) ? 8 : Currency.NumberFormat.CurrencyDecimalDigits;
+ }
+
+ ///
+ /// The internal unrounded raw amount
+ ///
+ public decimal Amount
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Rounds the amount to the number of significant decimal digits
+ /// of the associated currency using MidpointRounding.AwayFromZero.
+ ///
+ public decimal RoundedAmount
+ {
+ get
+ {
+ return decimal.Round(Amount, DecimalDigits, MidpointRounding.AwayFromZero);
+ }
+ }
+
+ ///
+ /// Truncates the amount to the number of significant decimal digits
+ /// of the associated currency.
+ ///
+ public decimal TruncatedAmount
+ {
+ get => (decimal)((long)Math.Truncate(Amount * DecimalDigits)) / DecimalDigits;
+ }
+
+ ///
+ /// The formatted amount
+ ///
+ public string Formatted
+ {
+ get => ToString(true, false);
+ }
+
+ private static void GuardCurrenciesAreEqual(Money a, Money b)
+ {
+ if (a.Currency != b.Currency)
+ throw new InvalidOperationException("Cannot operate on money values with different currencies.");
+ }
+
+ #region Compare
+
+ public override int GetHashCode()
+ {
+ if (Amount == 0)
+ return 0;
+
+ return Amount.GetHashCode() ^ Currency.GetHashCode();
+ }
+
+ public int CompareTo(Money other)
+ {
+ return ((IComparable)this).CompareTo(other);
+ }
+
+ int IComparable.CompareTo(object obj)
+ {
+ if (obj == null || !(obj is Money))
+ return 1;
+
+ Money other = (Money)obj;
+
+ if (this.Amount == other.Amount)
+ return 0;
+ if (this.Amount < other.Amount)
+ return -1;
+
+ return 1;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as Money);
+ }
+
+ bool IEquatable.Equals(Money other)
+ {
+ if (other == null)
+ return false;
+
+ if (ReferenceEquals(this, other))
+ return true;
+
+ if (other.Amount == 0 && this.Amount == 0)
+ return true;
+
+ return other.Amount == this.Amount && other.Currency == this.Currency;
+ }
+
+ public static bool operator ==(Money a, Money b) => a.Equals(b);
+ public static bool operator !=(Money a, Money b) => !a.Equals(b);
+
+ public static bool operator >(Money a, Money b)
+ {
+ GuardCurrenciesAreEqual(a, b);
+ return a.Amount > b.Amount;
+ }
+
+ public static bool operator <(Money a, Money b)
+ {
+ GuardCurrenciesAreEqual(a, b);
+ return a.Amount < b.Amount;
+ }
+
+ public static bool operator <=(Money a, Money b)
+ {
+ GuardCurrenciesAreEqual(a, b);
+ return a.Amount <= b.Amount;
+
+ }
+
+ public static bool operator >=(Money a, Money b)
+ {
+ GuardCurrenciesAreEqual(a, b);
+ return a.Amount >= b.Amount;
+
+ }
+
+ public static bool operator ==(Money a, int b) => a.Amount == b;
+ public static bool operator !=(Money a, int b) => a.Amount != b;
+ public static bool operator >(Money a, int b) => a.Amount > b;
+ public static bool operator <(Money a, int b) => a.Amount < b;
+ public static bool operator <=(Money a, int b) => a.Amount <= b;
+ public static bool operator >=(Money a, int b) => a.Amount >= b;
+
+ public static bool operator ==(Money a, float b) => a.Amount == (decimal)b;
+ public static bool operator !=(Money a, float b) => a.Amount != (decimal)b;
+ public static bool operator >(Money a, float b) => a.Amount > (decimal)b;
+ public static bool operator <(Money a, float b) => a.Amount < (decimal)b;
+ public static bool operator <=(Money a, float b) => a.Amount <= (decimal)b;
+ public static bool operator >=(Money a, float b) => a.Amount >= (decimal)b;
+
+ public static bool operator ==(Money a, double b) => a.Amount == (decimal)b;
+ public static bool operator !=(Money a, double b) => a.Amount != (decimal)b;
+ public static bool operator >(Money a, double b) => a.Amount > (decimal)b;
+ public static bool operator <(Money a, double b) => a.Amount < (decimal)b;
+ public static bool operator <=(Money a, double b) => a.Amount <= (decimal)b;
+ public static bool operator >=(Money a, double b) => a.Amount >= (decimal)b;
+
+ public static bool operator ==(Money a, decimal b) => a.Amount == b;
+ public static bool operator !=(Money a, decimal b) => a.Amount != b;
+ public static bool operator >(Money a, decimal b) => a.Amount > b;
+ public static bool operator <(Money a, decimal b) => a.Amount < b;
+ public static bool operator <=(Money a, decimal b) => a.Amount <= b;
+ public static bool operator >=(Money a, decimal b) => a.Amount >= b;
+
+ #endregion
+
+ #region Format
+
+ string IFormattable.ToString(string format, IFormatProvider formatProvider)
+ {
+ return this.ToString(!HideCurrency, false);
+ }
+
+ string IConvertible.ToString(IFormatProvider provider)
+ {
+ return this.ToString(!HideCurrency, false);
+ }
+
+ public override string ToString()
+ {
+ return this.ToString(!HideCurrency, false);
+ }
+
+ public string ToString(bool showCurrency)
+ {
+ return this.ToString(showCurrency, false);
+ }
+
+ public string ToString(bool showCurrency, bool useISOCodeAsSymbol)
+ {
+ var fmt = Currency.NumberFormat;
+
+ if (Currency.CustomFormatting.HasValue())
+ {
+ return RoundedAmount.ToString(Currency.CustomFormatting, fmt);
+ }
+ else
+ {
+ if (!showCurrency || useISOCodeAsSymbol)
+ {
+ fmt = (NumberFormatInfo)Currency.NumberFormat.Clone();
+ fmt.CurrencySymbol = !showCurrency ? "" : Currency.CurrencyCode;
+ }
+
+ return RoundedAmount.ToString("C", fmt);
+ }
+ }
+
+ #endregion
+
+ #region Convert
+
+ // For truthy checks in templating
+ public static explicit operator bool(Money money) => money.Amount != 0;
+ public static explicit operator string(Money money) => money.ToString(true, false);
+ public static explicit operator byte(Money money) => System.Convert.ToByte(money.Amount);
+ public static explicit operator decimal(Money money) => money.Amount;
+ public static explicit operator double(Money money) => System.Convert.ToDouble(money.Amount);
+ public static explicit operator float(Money money) => System.Convert.ToSingle(money.Amount);
+ public static explicit operator int(Money money) => System.Convert.ToInt32(money.Amount);
+ public static explicit operator long(Money money) => System.Convert.ToInt64(money.Amount);
+ public static explicit operator sbyte(Money money) => System.Convert.ToSByte(money.Amount);
+ public static explicit operator short(Money money) => System.Convert.ToInt16(money.Amount);
+ public static explicit operator ushort(Money money) => System.Convert.ToUInt16(money.Amount);
+ public static explicit operator uint(Money money) => System.Convert.ToUInt32(money.Amount);
+ public static explicit operator ulong(Money money) => System.Convert.ToUInt64(money.Amount);
+
+ TypeCode IConvertible.GetTypeCode() => TypeCode.Decimal;
+ object IConvertible.ToType(Type conversionType, IFormatProvider provider) => System.Convert.ChangeType(this.Amount, conversionType, provider);
+ bool IConvertible.ToBoolean(IFormatProvider provider) => Amount != 0;
+ char IConvertible.ToChar(IFormatProvider provider) => throw Error.InvalidCast(typeof(Money), typeof(char));
+ DateTime IConvertible.ToDateTime(IFormatProvider provider) => throw Error.InvalidCast(typeof(Money), typeof(DateTime));
+ byte IConvertible.ToByte(IFormatProvider provider) => (byte)this.Amount;
+ decimal IConvertible.ToDecimal(IFormatProvider provider) => this.Amount;
+ double IConvertible.ToDouble(IFormatProvider provider) => (double)this.Amount;
+ short IConvertible.ToInt16(IFormatProvider provider) => (short)this.Amount;
+ int IConvertible.ToInt32(IFormatProvider provider) => (int)this.Amount;
+ long IConvertible.ToInt64(IFormatProvider provider) => (long)this.Amount;
+ sbyte IConvertible.ToSByte(IFormatProvider provider) => (sbyte)this.Amount;
+ float IConvertible.ToSingle(IFormatProvider provider) => (float)this.Amount;
+ ushort IConvertible.ToUInt16(IFormatProvider provider) => (ushort)this.Amount;
+ uint IConvertible.ToUInt32(IFormatProvider provider) => (uint)this.Amount;
+ ulong IConvertible.ToUInt64(IFormatProvider provider) => (ulong)this.Amount;
+
+ #endregion
+
+ #region Add
+
+ public static Money operator ++(Money a)
+ {
+ a.Amount++;
+ return a;
+ }
+
+ public static Money operator +(Money a, Money b)
+ {
+ GuardCurrenciesAreEqual(a, b);
+ return new Money(a.Amount + b.Amount, a.Currency);
+ }
+
+ public static Money operator +(Money a, int b) => a + (decimal)b;
+ public static Money operator +(Money a, float b) => a + (decimal)b;
+ public static Money operator +(Money a, double b) => a + (decimal)b;
+ public static Money operator +(Money a, decimal b) => new Money(a.Amount + b, a.Currency);
+
+ #endregion
+
+ #region Substract
+
+ public static Money operator --(Money a)
+ {
+ a.Amount--;
+ return a;
+ }
+
+ public static Money operator -(Money a, Money b)
+ {
+ GuardCurrenciesAreEqual(a, b);
+ return new Money(a.Amount - b.Amount, a.Currency);
+ }
+
+ public static Money operator -(Money a, int b) => a + (decimal)b;
+ public static Money operator -(Money a, float b) => a + (decimal)b;
+ public static Money operator -(Money a, double b) => a + (decimal)b;
+ public static Money operator -(Money a, decimal b) => new Money(a.Amount - b, a.Currency);
+
+ #endregion
+
+ #region Multiply
+
+ public static Money operator *(Money a, Money b)
+ {
+ GuardCurrenciesAreEqual(a, b);
+ return new Money(a.Amount - b.Amount, a.Currency);
+ }
+
+ public static Money operator *(Money a, int b) => a * (decimal)b;
+ public static Money operator *(Money a, float b) => a * (decimal)b;
+ public static Money operator *(Money a, double b) => a * (decimal)b;
+ public static Money operator *(Money a, decimal b) => new Money(a.Amount * b, a.Currency);
+
+ #endregion
+
+ #region Divide
+
+ public static Money operator /(Money a, Money b)
+ {
+ GuardCurrenciesAreEqual(a, b);
+ return new Money(a.Amount / b.Amount, a.Currency);
+ }
+
+ public static Money operator /(Money a, int b) => a / (decimal)b;
+ public static Money operator /(Money a, float b) => a / (decimal)b;
+ public static Money operator /(Money a, double b) => a / (decimal)b;
+ public static Money operator /(Money a, decimal b) => new Money(a.Amount / b, a.Currency);
+
+ #endregion
+
+ #region Exchange & Math
+
+ /////
+ ///// Rounds the amount if enabled for the currency or if is true
+ /////
+ ///// Round also if disabled for the currency
+ ///// A new instance with the rounded amount
+ //public Money Round(bool force = false)
+ //{
+
+ //}
+
+ public Money ConvertTo(Currency toCurrency)
+ {
+ if (Currency == toCurrency)
+ return this;
+
+ return new Money((Amount * Currency.Rate) / toCurrency.Rate, toCurrency);
+ }
+
+ ///
+ /// Evenly distributes the amount over n parts, resolving remainders that occur due to rounding
+ /// errors, thereby garuanteeing the postcondition: result->sum(r|r.amount) = this.amount and
+ /// x elements in result are greater than at least one of the other elements, where x = amount mod n.
+ ///
+ /// Number of parts over which the amount is to be distibuted.
+ /// Array with distributed Money amounts.
+ public Money[] Allocate(int n)
+ {
+ var cents = Math.Pow(10, DecimalDigits);
+ var lowResult = ((long)Math.Truncate((double)Amount / n * cents)) / cents;
+ var highResult = lowResult + 1.0d / cents;
+ var results = new Money[n];
+ var remainder = (int)(((double)Amount * cents) % n);
+
+ for (var i = 0; i < remainder; i++)
+ results[i] = new Money((decimal)highResult, Currency);
+
+ for (var i = remainder; i < n; i++)
+ results[i] = new Money((decimal)lowResult, Currency);
+
+ return results;
+ }
+
+ ///
+ /// Gets the ratio of one money to another.
+ ///
+ /// The numerator of the operation.
+ /// The denominator of the operation.
+ /// A decimal from 0.0 to 1.0 of the ratio between the two money values.
+ public static decimal GetRatio(Money numerator, Money denominator)
+ {
+ if (numerator == 0)
+ return 0;
+
+ if (denominator == 0)
+ throw new DivideByZeroException("Attempted to divide by zero!");
+
+ GuardCurrenciesAreEqual(numerator, denominator);
+
+ return numerator.Amount / denominator.Amount;
+ }
+
+ ///
+ /// Gets the smallest money, given the two values.
+ ///
+ /// The first money to compare.
+ /// The second money to compare.
+ /// The smallest money value of the arguments.
+ public static Money Min(Money a, Money b)
+ {
+ GuardCurrenciesAreEqual(a, b);
+
+ if (a == b)
+ return a;
+ else if (a > b)
+ return b;
+ else
+ return a;
+ }
+
+ ///
+ /// Gets the largest money, given the two values.
+ ///
+ /// The first money to compare.
+ /// The second money to compare.
+ /// The largest money value of the arguments.
+ public static Money Max(Money a, Money b)
+ {
+ GuardCurrenciesAreEqual(a, b);
+
+ if (a == b)
+ return a;
+ else if (a > b)
+ return a;
+ else
+ return b;
+ }
+
+ ///
+ /// Gets the absolute value of the .
+ ///
+ /// The value of money to convert.
+ /// The money value as an absolute value.
+ public static Money Abs(Money value)
+ {
+ return new Money(Math.Abs(value.Amount), value.Currency);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Packaging/PackagingUtils.cs b/src/Libraries/SmartStore.Core/Packaging/PackagingUtils.cs
index 91657a7900..0d50f65851 100644
--- a/src/Libraries/SmartStore.Core/Packaging/PackagingUtils.cs
+++ b/src/Libraries/SmartStore.Core/Packaging/PackagingUtils.cs
@@ -9,10 +9,8 @@
namespace SmartStore.Core.Packaging
{
-
public static class PackagingUtils
- {
-
+ {
public static string GetExtensionPrefix(string extensionType)
{
return string.Format("SmartStore.{0}.", extensionType);
@@ -23,8 +21,6 @@ public static string BuildPackageId(string extensionName, string extensionType)
return GetExtensionPrefix(extensionType) + extensionName;
}
-
-
internal static bool IsTheme(this IPackage package)
{
return IsTheme(package.Id);
diff --git a/src/Libraries/SmartStore.Core/PagedList`T.cs b/src/Libraries/SmartStore.Core/PagedList`T.cs
index 1614557a1e..dcebc38642 100644
--- a/src/Libraries/SmartStore.Core/PagedList`T.cs
+++ b/src/Libraries/SmartStore.Core/PagedList`T.cs
@@ -75,6 +75,14 @@ public IQueryable SourceQuery
get { return _query; }
}
+ public IPagedList AlterQuery(Func, IQueryable> alterer)
+ {
+ var result = alterer?.Invoke(_query);
+ _query = result ?? throw new InvalidOperationException("The '{0}' delegate must not return NULL.".FormatInvariant(nameof(alterer)));
+
+ return this;
+ }
+
public IQueryable ApplyPaging(IQueryable query)
{
if (_pageIndex == 0 && _pageSize == int.MaxValue)
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/Plugins/Providers/IProvider.cs b/src/Libraries/SmartStore.Core/Plugins/Providers/IProvider.cs
index f0dc56eb1a..fae747723b 100644
--- a/src/Libraries/SmartStore.Core/Plugins/Providers/IProvider.cs
+++ b/src/Libraries/SmartStore.Core/Plugins/Providers/IProvider.cs
@@ -1,9 +1,4 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Web.Routing;
namespace SmartStore.Core.Plugins
{
diff --git a/src/Libraries/SmartStore.Core/Search/DefaultIndexManager.cs b/src/Libraries/SmartStore.Core/Search/DefaultIndexManager.cs
index da73ff4841..0fe4f85550 100644
--- a/src/Libraries/SmartStore.Core/Search/DefaultIndexManager.cs
+++ b/src/Libraries/SmartStore.Core/Search/DefaultIndexManager.cs
@@ -4,7 +4,7 @@
namespace SmartStore.Core.Search
{
- public class DefaultIndexManager : IIndexManager
+ public class DefaultIndexManager : IIndexManager
{
private readonly IEnumerable> _providers;
@@ -13,14 +13,14 @@ public DefaultIndexManager(IEnumerable> providers)
_providers = providers;
}
- public bool HasAnyProvider(bool activeOnly = true)
+ public bool HasAnyProvider(string scope, bool activeOnly = true)
{
- return _providers.Any(x => !activeOnly || x.Value.IsActive);
+ return _providers.Any(x => !activeOnly || x.Value.IsActive(scope));
}
- public IIndexProvider GetIndexProvider(bool activeOnly = true)
+ public IIndexProvider GetIndexProvider(string scope, bool activeOnly = true)
{
- return _providers.FirstOrDefault(x => !activeOnly || x.Value.IsActive)?.Value;
+ return _providers.FirstOrDefault(x => !activeOnly || x.Value.IsActive(scope))?.Value;
}
}
}
diff --git a/src/Libraries/SmartStore.Core/Search/Events/IndexSegmentProcessedEvent.cs b/src/Libraries/SmartStore.Core/Search/Events/IndexSegmentProcessedEvent.cs
new file mode 100644
index 0000000000..b235921138
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Search/Events/IndexSegmentProcessedEvent.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+
+namespace SmartStore.Core.Search
+{
+ public class IndexSegmentProcessedEvent
+ {
+ public IndexSegmentProcessedEvent(string scope, IEnumerable operations, bool isRebuild)
+ {
+ Guard.NotEmpty(scope, nameof(scope));
+ Guard.NotNull(operations, nameof(operations));
+
+ Scope = scope;
+ IsRebuild = isRebuild;
+ Operations = operations;
+ Metadata = new Dictionary();
+ }
+
+ public string Scope
+ {
+ get;
+ private set;
+ }
+
+ public bool IsRebuild
+ {
+ get;
+ private set;
+ }
+
+ public IEnumerable Operations
+ {
+ get;
+ private set;
+ }
+
+ public IDictionary Metadata
+ {
+ get;
+ private set;
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Search/Events/IndexingCompletedEvent.cs b/src/Libraries/SmartStore.Core/Search/Events/IndexingCompletedEvent.cs
new file mode 100644
index 0000000000..b1304aae56
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Search/Events/IndexingCompletedEvent.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace SmartStore.Core.Search
+{
+ public class IndexingCompletedEvent
+ {
+ public IndexingCompletedEvent(IndexInfo indexInfo, bool wasRebuilt)
+ {
+ Guard.NotNull(indexInfo, nameof(indexInfo));
+
+ IndexInfo = indexInfo;
+ WasRebuilt = wasRebuilt;
+ }
+
+ public IndexInfo IndexInfo
+ {
+ get;
+ private set;
+ }
+
+ public bool WasRebuilt
+ {
+ get;
+ private set;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Libraries/SmartStore.Core/Search/Facets/FacetDescriptor.cs b/src/Libraries/SmartStore.Core/Search/Facets/FacetDescriptor.cs
index bef397ea48..d0e8b5b8e7 100644
--- a/src/Libraries/SmartStore.Core/Search/Facets/FacetDescriptor.cs
+++ b/src/Libraries/SmartStore.Core/Search/Facets/FacetDescriptor.cs
@@ -29,34 +29,6 @@ public FacetDescriptor(string key)
Key = key;
}
- ///
- /// Gets the string resource key for a facet group kind
- ///
- /// Facet group kind
- /// Resource key
- public static string GetLabelResourceKey(FacetGroupKind kind)
- {
- switch (kind)
- {
- case FacetGroupKind.Category:
- return "Search.Facet.Category";
- case FacetGroupKind.Brand:
- return "Search.Facet.Manufacturer";
- case FacetGroupKind.Price:
- return "Search.Facet.Price";
- case FacetGroupKind.Rating:
- return "Search.Facet.Rating";
- case FacetGroupKind.DeliveryTime:
- return "Search.Facet.DeliveryTime";
- case FacetGroupKind.Availability:
- return "Search.Facet.Availability";
- case FacetGroupKind.NewArrivals:
- return "Search.Facet.NewArrivals";
- default:
- return null;
- }
- }
-
///
/// Gets the key / field name.
///
diff --git a/src/Libraries/SmartStore.Core/Search/Facets/FacetGroup.cs b/src/Libraries/SmartStore.Core/Search/Facets/FacetGroup.cs
index cb0bd028ec..e6d9ffc48e 100644
--- a/src/Libraries/SmartStore.Core/Search/Facets/FacetGroup.cs
+++ b/src/Libraries/SmartStore.Core/Search/Facets/FacetGroup.cs
@@ -16,7 +16,10 @@ public enum FacetGroupKind
Availability,
NewArrivals,
Attribute,
- Variant
+ Variant,
+ Forum,
+ Customer,
+ Date
}
[DebuggerDisplay("Key: {Key}, Label: {Label}, Kind: {Kind}")]
@@ -26,11 +29,12 @@ public class FacetGroup
private FacetGroupKind? _kind;
public FacetGroup()
- : this (string.Empty, string.Empty, false, false, 0, Enumerable.Empty())
+ : this (string.Empty, string.Empty, string.Empty, false, false, 0, Enumerable.Empty())
{
}
public FacetGroup(
+ string scope,
string key,
string label,
bool isMultiSelect,
@@ -38,13 +42,16 @@ public FacetGroup(
int displayOrder,
IEnumerable facets)
{
- Guard.NotNull(key, nameof(key));
+ Guard.NotNull(scope, nameof(scope));
+ Guard.NotNull(key, nameof(key));
Guard.NotNull(facets, nameof(facets));
+ Scope = scope;
Key = key;
Label = label;
IsMultiSelect = isMultiSelect;
DisplayOrder = displayOrder;
+ IsScrollable = true;
_facets = new Dictionary(StringComparer.OrdinalIgnoreCase);
@@ -57,14 +64,14 @@ public FacetGroup(
{
_facets.Add(x.Key, x);
}
- catch (Exception exception)
+ catch (Exception ex)
{
- exception.Dump();
+ ex.Dump();
}
});
}
- public static FacetGroupKind GetKindByKey(string key)
+ public static FacetGroupKind GetKindByKey(string scope, string key)
{
if (key.StartsWith("attrid"))
{
@@ -74,41 +81,40 @@ public static FacetGroupKind GetKindByKey(string key)
{
return FacetGroupKind.Variant;
}
- else if (key == "categoryid" || key == "notfeaturedcategoryid")
- {
- return FacetGroupKind.Category;
- }
- else if (key == "manufacturerid")
- {
- return FacetGroupKind.Brand;
- }
- else if (key == "price")
- {
- return FacetGroupKind.Price;
- }
- else if (key == "rating")
- {
- return FacetGroupKind.Rating;
- }
- else if (key == "deliveryid")
- {
- return FacetGroupKind.DeliveryTime;
- }
- else if (key == "available")
- {
- return FacetGroupKind.Availability;
- }
- else if (key == "createdon")
- {
- return FacetGroupKind.NewArrivals;
- }
- else
- {
- return FacetGroupKind.Unknown;
- }
- }
- public string Key
+ switch (key)
+ {
+ case "categoryid":
+ case "notfeaturedcategoryid":
+ return FacetGroupKind.Category;
+ case "manufacturerid":
+ return FacetGroupKind.Brand;
+ case "price":
+ return FacetGroupKind.Price;
+ case "rating":
+ return FacetGroupKind.Rating;
+ case "deliveryid":
+ return FacetGroupKind.DeliveryTime;
+ case "available":
+ return FacetGroupKind.Availability;
+ case "createdon":
+ return scope == "Catalog" ? FacetGroupKind.NewArrivals : FacetGroupKind.Date;
+ case "forumid":
+ return FacetGroupKind.Forum;
+ case "customerid":
+ return FacetGroupKind.Customer;
+ default:
+ return FacetGroupKind.Unknown;
+ }
+ }
+
+ public string Scope
+ {
+ get;
+ private set;
+ }
+
+ public string Key
{
get;
private set;
@@ -126,7 +132,7 @@ public bool IsMultiSelect
private set;
}
- public bool HasChildren
+ public bool HasChildren
{
get;
private set;
@@ -138,7 +144,13 @@ public int DisplayOrder
private set;
}
- public IEnumerable Facets
+ public bool IsScrollable
+ {
+ get;
+ set;
+ }
+
+ public IEnumerable Facets
{
get
{
@@ -170,7 +182,7 @@ public FacetGroupKind Kind
{
if (_kind == null)
{
- _kind = GetKindByKey(Key);
+ _kind = GetKindByKey(Scope, Key);
}
return _kind.Value;
diff --git a/src/Libraries/SmartStore.Core/Search/Facets/FacetValue.cs b/src/Libraries/SmartStore.Core/Search/Facets/FacetValue.cs
index 0cb8d6d3e1..e3e451cab7 100644
--- a/src/Libraries/SmartStore.Core/Search/Facets/FacetValue.cs
+++ b/src/Libraries/SmartStore.Core/Search/Facets/FacetValue.cs
@@ -155,20 +155,38 @@ public override bool Equals(object obj)
return this.Equals(obj as FacetValue);
}
+ protected virtual string ConvertToString(object value)
+ {
+ if (value != null)
+ {
+ if (TypeCode == IndexTypeCode.DateTime)
+ {
+ // The default conversion is not pretty enough.
+ var dt = (DateTime)value;
+ if (dt.Hour == 0 && dt.Minute == 0 && dt.Second == 0)
+ {
+ return dt.ToString("yyyy-MM-dd");
+ }
+ else
+ {
+ return dt.ToString("yyyy-MM-dd HH:mm:ss");
+ }
+ }
+
+ return Convert.ToString(value, CultureInfo.InvariantCulture);
+ }
+
+ return string.Empty;
+ }
+
public override string ToString()
{
var result = string.Empty;
-
- var valueStr = Value != null
- ? Convert.ToString(Value, CultureInfo.InvariantCulture)
- : string.Empty;
+ var valueStr = ConvertToString(Value);
if (IsRange)
{
- var upperValueStr = UpperValue != null
- ? Convert.ToString(UpperValue, CultureInfo.InvariantCulture)
- : string.Empty;
-
+ var upperValueStr = ConvertToString(UpperValue);
if (upperValueStr.HasValue())
{
result = string.Concat(valueStr, "~", upperValueStr);
diff --git a/src/Libraries/SmartStore.Core/Search/IIndexManager.cs b/src/Libraries/SmartStore.Core/Search/IIndexManager.cs
index da56fc1280..72868d85b5 100644
--- a/src/Libraries/SmartStore.Core/Search/IIndexManager.cs
+++ b/src/Libraries/SmartStore.Core/Search/IIndexManager.cs
@@ -1,25 +1,25 @@
-using System;
-
-namespace SmartStore.Core.Search
+namespace SmartStore.Core.Search
{
- ///
- /// A simple factory for registered search index providers
- ///
- public interface IIndexManager
+ ///
+ /// A simple factory for registered search index providers
+ ///
+ public interface IIndexManager
{
- ///
- /// Whether at least one provider is available, which implements
- ///
- /// Whether only active providers should be queried for
- /// true if at least one provider is registered, false ortherwise
- /// Primarily used to skip indexing processes
- bool HasAnyProvider(bool activeOnly = true);
+ ///
+ /// Whether at least one provider is available, which implements
+ ///
+ /// Index scope name
+ /// Whether only active providers should be queried for
+ /// true if at least one provider is registered, false ortherwise
+ /// Primarily used to skip indexing processes
+ bool HasAnyProvider(string scope, bool activeOnly = true);
///
/// Returns the instance of the first registered index provider (e.g. a Lucene provider)
///
+ /// Index scope name
/// Whether only active providers should be queried for
/// The index provider implementation instance
- IIndexProvider GetIndexProvider(bool activeOnly = true);
+ IIndexProvider GetIndexProvider(string scope, bool activeOnly = true);
}
}
diff --git a/src/Libraries/SmartStore.Core/Search/IIndexOperation.cs b/src/Libraries/SmartStore.Core/Search/IIndexOperation.cs
new file mode 100644
index 0000000000..27453c616e
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Search/IIndexOperation.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SmartStore.Core.Search
+{
+ public enum IndexOperationType
+ {
+ Index,
+ Delete
+ }
+
+ ///
+ /// Represents an indexing operation
+ ///
+ public interface IIndexOperation
+ {
+ ///
+ /// The type of the operation
+ ///
+ IndexOperationType OperationType { get; }
+
+ ///
+ /// The document being inserted to or deleted from the index storage
+ ///
+ IIndexDocument Document { get; }
+
+ ///
+ /// The database entity from which was created
+ ///
+ BaseEntity Entity { get; }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Search/IIndexProvider.cs b/src/Libraries/SmartStore.Core/Search/IIndexProvider.cs
index e6c0825057..b63ac0261c 100644
--- a/src/Libraries/SmartStore.Core/Search/IIndexProvider.cs
+++ b/src/Libraries/SmartStore.Core/Search/IIndexProvider.cs
@@ -4,16 +4,16 @@ namespace SmartStore.Core.Search
{
public interface IIndexProvider
{
- ///
- /// Gets a value indicating whether the index provider is active
- ///
- bool IsActive { get; }
+ ///
+ /// Gets a value indicating whether the search index given by scope is active
+ ///
+ bool IsActive(string scope);
- ///
- /// Enumerates the names of all EXISTING indexes.
- /// A name is required for the method.
- ///
- IEnumerable EnumerateIndexes();
+ ///
+ /// Enumerates the names of all EXISTING indexes.
+ /// A name is required for the method.
+ ///
+ IEnumerable EnumerateIndexes();
///
/// Creates an empty document
diff --git a/src/Libraries/SmartStore.Core/Search/ISearchEngine.cs b/src/Libraries/SmartStore.Core/Search/ISearchEngine.cs
index 70e9dcb508..2a17f673b0 100644
--- a/src/Libraries/SmartStore.Core/Search/ISearchEngine.cs
+++ b/src/Libraries/SmartStore.Core/Search/ISearchEngine.cs
@@ -47,13 +47,25 @@ public interface ISearchEngine
/// Suggestions/corrections or an empty array
string[] CheckSpelling();
- ///
- /// Highlights chosen terms in a text, extracting the most relevant sections
- ///
- /// Text to highlight terms in
- /// Text/HTML to prepend to matched keyword
- /// Text/HTML to append to matched keyword
- /// Highlighted text fragments
- string Highlight(string input, string preMatch, string postMatch);
- }
+ ///
+ /// Highlights chosen terms in a text, extracting the most relevant sections
+ ///
+ /// Text to highlight terms in
+ /// Field name
+ /// Text/HTML to prepend to matched keyword
+ /// Text/HTML to append to matched keyword
+ /// Highlighted text fragments
+ string Highlight(string input, string fieldName, string preMatch, string postMatch);
+
+ ///
+ /// Gets highlighted text fragments for an entity identifier.
+ ///
+ /// Entity identifier.
+ /// Field name.
+ /// Text/HTML to prepend to matched keyword.
+ /// Text/HTML to append to matched keyword.
+ /// Maximum number of returned text fragments.
+ /// Highlighted text fragments.
+ string Highlight(int id, string fieldName, string preMatch, string postMatch, int maxNumFragments);
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Search/ISearchQuery.cs b/src/Libraries/SmartStore.Core/Search/ISearchQuery.cs
index cf1bf1cca3..28ba070494 100644
--- a/src/Libraries/SmartStore.Core/Search/ISearchQuery.cs
+++ b/src/Libraries/SmartStore.Core/Search/ISearchQuery.cs
@@ -3,16 +3,16 @@
namespace SmartStore.Core.Search
{
- public interface ISearchQuery
+ public interface ISearchQuery
{
// Language, Currency & Store
int? LanguageId { get; }
string LanguageCulture { get; }
string CurrencyCode { get; }
- int? StoreId { get; }
+ int? StoreId { get; }
- // Search term
- string[] Fields { get; }
+ // Search term
+ string[] Fields { get; }
string Term { get; }
SearchMode Mode { get; }
bool EscapeTerm { get; }
diff --git a/src/Libraries/SmartStore.Core/Search/IndexInfo.cs b/src/Libraries/SmartStore.Core/Search/IndexInfo.cs
new file mode 100644
index 0000000000..468754317f
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Search/IndexInfo.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace SmartStore.Core.Search
+{
+ public enum IndexingStatus
+ {
+ Unavailable = -1,
+ Idle = 0,
+ Rebuilding = 1,
+ Updating = 2
+ }
+
+ public class IndexInfo
+ {
+ public IndexInfo(string scope)
+ {
+ Guard.NotEmpty(scope, nameof(scope));
+
+ Scope = scope;
+ Fields = new string[0];
+ }
+
+ public string Scope { get; private set; }
+ public int DocumentCount { get; set; }
+ public IEnumerable Fields { get; set; }
+
+ // loaded from status file
+ public DateTime? LastIndexedUtc { get; set; }
+ public TimeSpan? LastIndexingDuration { get; set; }
+ public bool IsModified { get; set; }
+ public IndexingStatus Status { get; set; }
+ public string Error { get; set; }
+
+ ///
+ /// Indicates that the index should be rebuilt from scratch, because
+ /// some global settings have changed (like tax rates for example)
+ ///
+ public bool ShouldRebuild { get; set; }
+
+ public string ToXml()
+ {
+ return new XDocument(
+ new XElement("info",
+ new XElement("status", Status),
+ new XElement("last-indexed-utc", LastIndexedUtc?.ToString("u")),
+ new XElement("last-indexing-duration", LastIndexingDuration?.ToString("c")),
+ new XElement("is-modified", IsModified ? "true" : "false"),
+ new XElement("error", Error),
+ new XElement("should-rebuild", ShouldRebuild ? "true" : "false"),
+ new XElement("document-count", DocumentCount),
+ new XElement("fields", string.Join(", ", Fields ?? Enumerable.Empty()))
+ )).ToString();
+ }
+
+ public static IndexInfo FromXml(string xml, string scope)
+ {
+ var info = new IndexInfo(scope);
+
+ try
+ {
+ var doc = XDocument.Parse(xml);
+
+ var lastIndexed = doc.Descendants("last-indexed-utc").FirstOrDefault()?.Value;
+ if (lastIndexed.HasValue())
+ {
+ info.LastIndexedUtc = lastIndexed.Convert()?.ToUniversalTime();
+ }
+
+ var lastDuration = doc.Descendants("last-indexing-duration").FirstOrDefault()?.Value;
+ if (lastDuration.HasValue())
+ {
+ info.LastIndexingDuration = lastDuration.Convert();
+ }
+
+ var isModified = doc.Descendants("is-modified").FirstOrDefault()?.Value;
+ if (isModified.HasValue())
+ {
+ info.IsModified = isModified.Convert();
+ }
+ else
+ {
+ info.IsModified = lastIndexed.HasValue();
+ }
+
+ var status = doc.Descendants("status").FirstOrDefault()?.Value;
+ if (status.HasValue())
+ {
+ info.Status = status.Convert();
+ }
+
+ info.Error = doc.Descendants("error").FirstOrDefault()?.Value;
+
+ var documentCount = doc.Descendants("document-count").FirstOrDefault()?.Value;
+ if (documentCount.HasValue())
+ {
+ info.DocumentCount = documentCount.ToInt();
+ }
+
+ var fields = doc.Descendants("fields").FirstOrDefault()?.Value;
+ if (fields.HasValue())
+ {
+ info.Fields = fields.SplitSafe(", ");
+ }
+
+ var shouldRebuild = doc.Descendants("should-rebuild").FirstOrDefault()?.Value;
+ if (shouldRebuild.HasValue())
+ {
+ info.ShouldRebuild = shouldRebuild.Convert();
+ }
+ }
+ catch { }
+
+ return info;
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Search/IndexProviderBase.cs b/src/Libraries/SmartStore.Core/Search/IndexProviderBase.cs
index 4b6caa0796..15662c3768 100644
--- a/src/Libraries/SmartStore.Core/Search/IndexProviderBase.cs
+++ b/src/Libraries/SmartStore.Core/Search/IndexProviderBase.cs
@@ -4,15 +4,12 @@ namespace SmartStore.Core.Search
{
public abstract class IndexProviderBase : IIndexProvider
{
- public virtual bool IsActive
- {
- get
- {
- return true;
- }
- }
+ public virtual bool IsActive(string scope)
+ {
+ return true;
+ }
- public abstract IEnumerable EnumerateIndexes();
+ public abstract IEnumerable EnumerateIndexes();
public virtual IIndexDocument CreateDocument(int id, SearchDocumentType? documentType)
{
diff --git a/src/Libraries/SmartStore.Core/Search/SearchDocumentType.cs b/src/Libraries/SmartStore.Core/Search/SearchDocumentType.cs
index fe80ff0036..c2597f571d 100644
--- a/src/Libraries/SmartStore.Core/Search/SearchDocumentType.cs
+++ b/src/Libraries/SmartStore.Core/Search/SearchDocumentType.cs
@@ -9,6 +9,9 @@ public enum SearchDocumentType
Attribute,
AttributeValue,
Variant,
- VariantValue
+ VariantValue,
+ Customer,
+ Forum,
+ ForumPost
}
}
diff --git a/src/Libraries/SmartStore.Core/Search/SearchQuery.cs b/src/Libraries/SmartStore.Core/Search/SearchQuery.cs
index 6cd4f78b9e..18cfcd3f83 100644
--- a/src/Libraries/SmartStore.Core/Search/SearchQuery.cs
+++ b/src/Libraries/SmartStore.Core/Search/SearchQuery.cs
@@ -7,7 +7,7 @@
namespace SmartStore.Core.Search
{
- public enum SearchResultFlags
+ public enum SearchResultFlags
{
WithHits = 1 << 0,
WithFacets = 1 << 1,
@@ -65,10 +65,10 @@ protected SearchQuery(string[] fields, string term, SearchMode mode = SearchMode
public int? LanguageId { get; protected set; }
public string LanguageCulture { get; protected set; }
public string CurrencyCode { get; protected set; }
- public int? StoreId { get; protected set; }
+ public int? StoreId { get; protected set; }
- // Search term
- public string[] Fields { get; set; }
+ // Search term
+ public string[] Fields { get; set; }
public string Term { get; set; }
public bool EscapeTerm { get; protected set; }
public SearchMode Mode { get; protected set; }
@@ -164,7 +164,7 @@ public TQuery Slice(int skip, int take)
public TQuery CheckSpelling(int maxSuggestions, int minQueryLength = 4, int maxHitCount = 3)
{
- Guard.IsPositive(maxSuggestions, nameof(maxSuggestions));
+ Guard.NotNegative(maxSuggestions, nameof(maxSuggestions));
Guard.IsPositive(minQueryLength, nameof(minQueryLength));
Guard.IsPositive(maxHitCount, nameof(maxHitCount));
diff --git a/src/Libraries/SmartStore.Core/Search/Settings/ForumSearchSettings.cs b/src/Libraries/SmartStore.Core/Search/Settings/ForumSearchSettings.cs
new file mode 100644
index 0000000000..b213e2e56b
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Search/Settings/ForumSearchSettings.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using SmartStore.Core.Configuration;
+using SmartStore.Core.Domain.Forums;
+
+namespace SmartStore.Core.Search
+{
+ public class ForumSearchSettings : ISettings
+ {
+ public ForumSearchSettings()
+ {
+ SearchMode = SearchMode.Contains;
+ SearchFields = new List { "username", "text" };
+ DefaultSortOrder = ForumTopicSorting.Relevance;
+ InstantSearchEnabled = true;
+ InstantSearchNumberOfHits = 10;
+ InstantSearchTermMinLength = 2;
+ FilterMinHitCount = 1;
+ FilterMaxChoicesCount = 20;
+
+ ForumDisplayOrder = 1;
+ CustomerDisplayOrder = 2;
+ DateDisplayOrder = 3;
+ }
+
+ ///
+ /// Gets or sets the search mode.
+ ///
+ public SearchMode SearchMode { get; set; }
+
+ ///
+ /// Gets or sets name of fields to be searched. The name field is always searched.
+ ///
+ public List SearchFields { get; set; }
+
+ ///
+ /// Gets or sets the default sort order in search results.
+ ///
+ public ForumTopicSorting DefaultSortOrder { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether instant-search is enabled.
+ ///
+ public bool InstantSearchEnabled { get; set; }
+
+ ///
+ /// Gets or sets the number of hits to return when using "instant-search" feature.
+ ///
+ public int InstantSearchNumberOfHits { get; set; }
+
+ ///
+ /// Gets or sets a minimum instant-search term length.
+ ///
+ public int InstantSearchTermMinLength { get; set; }
+
+ ///
+ /// Gets or sets the minimum hit count for a filter value. Values with a lower hit count are not displayed.
+ ///
+ public int FilterMinHitCount { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of filter values to be displayed.
+ ///
+ public int FilterMaxChoicesCount { get; set; }
+
+ #region Common facet settings
+
+ public bool ForumDisabled { get; set; }
+ public bool CustomerDisabled { get; set; }
+ public bool DateDisabled { get; set; }
+
+ public int ForumDisplayOrder { get; set; }
+ public int CustomerDisplayOrder { get; set; }
+ public int DateDisplayOrder { get; set; }
+
+ #endregion
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Search/SearchSettings.cs b/src/Libraries/SmartStore.Core/Search/Settings/SearchSettings.cs
similarity index 100%
rename from src/Libraries/SmartStore.Core/Search/SearchSettings.cs
rename to src/Libraries/SmartStore.Core/Search/Settings/SearchSettings.cs
diff --git a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj
index 023bb38722..98f3bb0cb0 100644
--- a/src/Libraries/SmartStore.Core/SmartStore.Core.csproj
+++ b/src/Libraries/SmartStore.Core/SmartStore.Core.csproj
@@ -182,6 +182,7 @@
+
@@ -209,6 +210,7 @@
+
@@ -216,6 +218,9 @@
+
+
+
@@ -228,19 +233,23 @@
+
+
+
+
-
+
@@ -254,6 +263,9 @@
+
+
+
@@ -266,6 +278,8 @@
+
+
@@ -273,6 +287,7 @@
+
@@ -429,6 +444,7 @@
+
@@ -440,11 +456,14 @@
+
+
+
@@ -456,7 +475,8 @@
-
+
+
@@ -505,7 +525,6 @@
-
@@ -516,7 +535,6 @@
-
diff --git a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs
index cb55e7d3ed..75f292f08f 100644
--- a/src/Libraries/SmartStore.Core/SmartStoreVersion.cs
+++ b/src/Libraries/SmartStore.Core/SmartStoreVersion.cs
@@ -6,7 +6,27 @@
namespace SmartStore.Core
{
- public static class SmartStoreVersion
+ public class HelpTopic
+ {
+ public static HelpTopic CronExpressions = new HelpTopic("cron", "Managing+Scheduled+Tasks#ManagingScheduledTasks-Cron", "Geplante+Aufgaben+verwalten#GeplanteAufgabenverwalten-CronAusdruck");
+
+ public HelpTopic(string name, string enPath, string dePath)
+ {
+ Guard.NotEmpty(name, nameof(name));
+ Guard.NotEmpty(enPath, nameof(enPath));
+ Guard.NotEmpty(dePath, nameof(dePath));
+
+ Name = name;
+ EnPath = enPath;
+ DePath = dePath;
+ }
+
+ public string Name { get; private set; }
+ public string EnPath { get; private set; }
+ public string DePath { get; private set; }
+ }
+
+ public static class SmartStoreVersion
{
private static readonly Version s_infoVersion = new Version("1.0.0.0");
private static readonly List s_breakingChangesHistory = new List
@@ -16,15 +36,17 @@ public static class SmartStoreVersion
// A release with breaking changes should definitely have at least
// a greater minor version.
new Version("1.2"),
- new Version("1.2.1"), // MC: had to be :-(
+ new Version("1.2.1"),
new Version("2.0"),
new Version("2.1"),
new Version("2.2"),
new Version("2.5"),
- new Version("3.0")
+ new Version("3.0"),
+ new Version("3.1"),
+ new Version("3.1.5")
};
- private const string HELP_BASEURL = "http://docs.smartstore.com/display/";
+ private const string HELP_BASEURL = "https://docs.smartstore.com/display/";
static SmartStoreVersion()
{
@@ -68,8 +90,19 @@ public static Version Version
}
}
+ public static string GenerateHelpUrl(string languageCode, HelpTopic topic)
+ {
+ Guard.NotEmpty(languageCode, nameof(languageCode));
+ Guard.NotNull(topic, nameof(topic));
+
+ var path = languageCode.IsCaseInsensitiveEqual("de") ? topic.DePath : topic.EnPath;
+ return GenerateHelpUrl(languageCode, path);
+ }
+
public static string GenerateHelpUrl(string languageCode, string path)
{
+ Guard.NotEmpty(languageCode, nameof(languageCode));
+
return String.Concat(
HELP_BASEURL,
GetUserGuideSpaceKey(languageCode),
@@ -79,10 +112,9 @@ public static string GenerateHelpUrl(string languageCode, string path)
public static string GetUserGuideSpaceKey(string languageCode)
{
- if(languageCode.Equals("de"))
- return "SDDE30";
-
- return "SMNET30";
+ return languageCode.IsCaseInsensitiveEqual("de")
+ ? "SDDE31"
+ : "SMNET31";
}
///
diff --git a/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs b/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs
index 6e3c043b89..5a4a2253b2 100644
--- a/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs
+++ b/src/Libraries/SmartStore.Core/Themes/DefaultThemeRegistry.cs
@@ -435,7 +435,8 @@ private void OnThemeFileChanged(string name, string fullPath, ThemeFileChangeTyp
RaiseBaseThemeChanged(baseThemeChangedArgs);
}
- RaiseThemeFileChanged(new ThemeFileChangedEventArgs {
+ RaiseThemeFileChanged(new ThemeFileChangedEventArgs
+ {
ChangeType = changeType,
FullPath = fullPath,
ThemeName = themeName,
diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeFolderData.cs b/src/Libraries/SmartStore.Core/Themes/ThemeFolderData.cs
index 3bac8d4084..e557505cc9 100644
--- a/src/Libraries/SmartStore.Core/Themes/ThemeFolderData.cs
+++ b/src/Libraries/SmartStore.Core/Themes/ThemeFolderData.cs
@@ -7,6 +7,7 @@ internal class ThemeFolderData : ITopologicSortable
{
public string FolderName { get; set; }
public string FullPath { get; set; }
+ public bool IsSymbolicLink { get; set; }
public string VirtualBasePath { get; set; }
public XmlDocument Configuration { get; set; }
public string BaseTheme { get; set; }
diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs b/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs
index 739c8a40b3..dbaca7e630 100644
--- a/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs
+++ b/src/Libraries/SmartStore.Core/Themes/ThemeManifest.cs
@@ -1,11 +1,8 @@
using System;
-using System.Linq;
using System.Collections.Generic;
using System.Xml;
using SmartStore.Collections;
using System.IO;
-using SmartStore.Utilities;
-using System.Web;
namespace SmartStore.Core.Themes
{
@@ -46,7 +43,15 @@ internal static ThemeFolderData CreateThemeFolderData(string themePath, string v
return null;
virtualBasePath = virtualBasePath.EnsureEndsWith("/");
+
var themeDirectory = new DirectoryInfo(themePath);
+
+ var isSymLink = themeDirectory.IsSymbolicLink();
+ if (isSymLink)
+ {
+ themeDirectory = new DirectoryInfo(themeDirectory.GetFinalPathName());
+ }
+
var themeConfigFile = new FileInfo(System.IO.Path.Combine(themeDirectory.FullName, "theme.config"));
if (themeConfigFile.Exists)
@@ -69,6 +74,7 @@ internal static ThemeFolderData CreateThemeFolderData(string themePath, string v
{
FolderName = themeDirectory.Name,
FullPath = themeDirectory.FullName,
+ IsSymbolicLink = isSymLink,
Configuration = doc,
VirtualBasePath = virtualBasePath,
BaseTheme = baseTheme
@@ -97,28 +103,32 @@ public string Location
protected internal set;
}
- ///
- /// Gets the physical path of the theme
- ///
- public string Path
- {
- get;
- protected internal set;
+ ///
+ /// Determines whether the theme directory is a symbolic link to another target.
+ ///
+ public bool IsSymbolicLink
+ {
+ get;
+ protected internal set;
}
- public string PreviewImageUrl
- {
- get;
- protected internal set;
+ ///
+ /// Gets the physical path of the theme. In case of a symbolic link,
+ /// returns the link's target path.
+ ///
+ public string Path
+ {
+ get;
+ protected internal set;
}
- public string PreviewText
+ public string PreviewImageUrl
{
get;
protected internal set;
}
- public bool SupportRtl
+ public string PreviewText
{
get;
protected internal set;
@@ -178,14 +188,16 @@ public IDictionary Variables
}
var baseVars = this.BaseTheme.Variables;
- var merged = new Dictionary(baseVars, StringComparer.OrdinalIgnoreCase);
+ var mergedVars = new Dictionary(baseVars, StringComparer.OrdinalIgnoreCase);
+ var newVars = new List();
+
foreach (var localVar in _variables)
{
- if (merged.ContainsKey(localVar.Key))
+ if (mergedVars.ContainsKey(localVar.Key))
{
// Overridden var in child: update existing.
- var baseVar = merged[localVar.Key];
- merged[localVar.Key] = new ThemeVariableInfo
+ var baseVar = mergedVars[localVar.Key];
+ mergedVars[localVar.Key] = new ThemeVariableInfo
{
Name = baseVar.Name,
Type = baseVar.Type,
@@ -196,11 +208,25 @@ public IDictionary Variables
}
else
{
- // New var in child: add to list.
- merged.Add(localVar.Key, localVar.Value);
+ // New var in child: add to temp list.
+ newVars.Add(localVar.Value);
}
}
+ var merged = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var newVar in newVars)
+ {
+ // New child theme vars must come first in final list
+ // to avoid wrong references in existing vars
+ merged.Add(newVar.Name, newVar);
+ }
+
+ foreach (var kvp in mergedVars)
+ {
+ merged.Add(kvp.Key, kvp.Value);
+ }
+
return merged;
}
internal set
diff --git a/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs b/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs
index 6fadd1910e..98f1058002 100644
--- a/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs
+++ b/src/Libraries/SmartStore.Core/Themes/ThemeManifestMaterializer.cs
@@ -22,6 +22,7 @@ public ThemeManifestMaterializer(ThemeFolderData folderData)
_manifest.BaseThemeName = folderData.BaseTheme;
_manifest.Location = folderData.VirtualBasePath;
_manifest.Path = folderData.FullPath;
+ _manifest.IsSymbolicLink = folderData.IsSymbolicLink;
_manifest.ConfigurationNode = folderData.Configuration.DocumentElement;
}
@@ -30,7 +31,6 @@ public ThemeManifest Materialize()
var root = _manifest.ConfigurationNode;
_manifest.ThemeTitle = root.GetAttribute("title").NullEmpty() ?? _manifest.ThemeName;
- _manifest.SupportRtl = root.GetAttribute("supportRTL").ToBool();
_manifest.PreviewImageUrl = root.GetAttribute("previewImageUrl").NullEmpty() ?? "~/Themes/{0}/preview.png".FormatCurrent(_manifest.ThemeName);
_manifest.PreviewText = root.GetAttribute("previewText").ToSafe();
_manifest.Author = root.GetAttribute("author").ToSafe();
diff --git a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs
index a6d0649e24..1c8aaad909 100644
--- a/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs
+++ b/src/Libraries/SmartStore.Core/Utilities/CommonHelper.cs
@@ -10,6 +10,9 @@
using System.Web.Hosting;
using System.Web.Mvc;
using SmartStore.ComponentModel;
+using System.Text;
+using Newtonsoft.Json;
+using System.Runtime.Serialization.Formatters.Binary;
namespace SmartStore.Utilities
{
@@ -265,5 +268,99 @@ public static bool IsTruthy(object value)
return true;
}
+
+ public static long GetObjectSizeInBytes(object obj, HashSet